Building Microservices to Someone Else's API Contract
The client's dashboard called services that no longer existed. I rebuilt them to match exactly what the dashboard expected, and it worked without a single edit.
By Wasif Rehman · · Web development
In short
- Read the consumer before designing the producer: its code, schema and config are the spec.
- Match the consumer's conventions, even where you would choose differently.
- Import the client's logic instead of reimplementing it, so two versions can never drift.
- Make orchestration a feature: one command to start, check and stop every service.
- Spend your testing budget on the seams between services.
Rebuilding missing microservices for an existing app
Most microservices posts start from a blank slate: design the services, then the contracts, then build. UmrahFlow, a freelance visa-automation project, handed me the opposite problem.
| Client repo | Its role |
|---|---|
| Next.js review dashboard | Where staff review extracted passports |
| Visa-submission console | Builds submission plans and runs visa submissions |
| WhatsApp relay | Brings pilgrim messages in from WhatsApp |
All three were working, and they referenced an ML services repo that didn't exist anymore. The dashboard was full of calls to an OCR service and assumptions about a background agent, with no implementation behind them.
Consumer-driven contracts when the consumer is the only spec
With no spec and no old code, the API contract had to be dug out of the caller. Consumer-driven contracts are usually described as a testing discipline. Here they were literally the only spec.
| Dashboard source | What it told me |
|---|---|
| Upload logic | The OCR response shape: snake_case fields nested under primary |
| Prisma schema | What the agent had to produce: Passport rows, AgentTrace rows, and messages flipped from processedAt = NULL |
| Environment variables | The ports each service was expected on |
That constraint sounds limiting, but it is clarifying. Every design question has an objective answer, and the answer is in someone else's code.
A request/response API and a background worker
The two rebuilt services ended up with very different shapes.
| Aspect | OCR service | Agent service |
|---|---|---|
| Shape | Classic request/response API in Python | One /healthz route and a background drain loop |
| Input | A multipart image | Unprocessed messages in Postgres |
| Output | Structured passport fields | OCR runs, database records and WhatsApp replies |
| Consumers | HTTP callers | Database rows |
The OCR service uses the MRZ and Claude vision merge described in Passport OCR with AI.
I resisted the urge to give the agent a REST surface. Its consumers are database rows, not HTTP callers. A poll loop against Postgres is simpler, crash-safe, and needs no retry logic on the sending side.
Reusing the client's code instead of reimplementing it
The third piece, the bridge, moves approved passports into the visa console as staged submission jobs. The console has intricate logic for building a valid submission plan. The worst thing I could do was reimplement it and let two versions drift.
So the bridge runs inside the console's own Python environment and imports its plan builder directly. The bridge stays a thin state-machine pump:
- Lease Pick up passports with the status
QUEUED. - Build Call their plan builder, not a copy of it.
- Stage Stage the submission job in the console.
Running five microservices with one command
Five services across two languages, two of them the client's, plus Postgres and a WhatsApp container. That is a fragile dev environment unless you make orchestration a feature.
A Makefile became the single control surface:
make up # start everything: Docker Compose for infra, native processes for the apps
make health # hit every health endpoint
make down # stop it allOne shared env file feeds every service, so a credential exists in exactly one place.
Smoke tests over unit tests for microservice seams
With this architecture the risk isn't inside any one service. It is in the seams. So the primary verification is an 11-check smoke script:
- Health Hit every health endpoint.
- A real agent turn Inject a message, watch a passport appear, and see the reply go out.
- Bridge staging Check that the bridge stages the job.
The reward was a drop-in replacement: the client's dashboard worked without a single edit, which was the whole assignment. The full pipeline is described in From WhatsApp photo to government portal.
Frequently asked questions
What is a consumer-driven contract?
It is an API contract defined by what the consumer expects, not by what the producer would prefer. It is usually described as a testing discipline. On UmrahFlow the consumer's code was the only spec I had.
How do you rebuild a service when its spec and code are gone?
Read the caller. The dashboard's upload logic gave me the OCR response shape, its Prisma schema showed what the agent had to write, and its environment variables gave the expected ports.
Does a background agent need a REST API?
Not if its consumers are database rows. The UmrahFlow agent service has only a /healthz route and a poll loop against Postgres, which is simpler, crash-safe, and needs no retry logic on the sending side.
Are smoke tests better than unit tests for microservices?
For a system like this one, the risk is in the seams between services. An end-to-end smoke script answers the integration question directly, where mocks can only answer it by inference.
How do you start several microservices with one command?
I used a Makefile: make up starts Docker Compose for the infrastructure and native processes for the apps, make health hits every health endpoint, and make down stops it all. One shared env file feeds every service.
Case study: UmrahFlow