One Contract, Many Clients
ForkTex Engineering · September 5, 2026 · 8 min
A backend and its clients agree on a contract. The question is only whether that agreement is written down somewhere a machine can check, or whether it lives in the heads of the people who happened to be in the room.
We write it down. The API's OpenAPI schema is the one contract, and every artefact that talks to it — the TypeScript data layer, the Python SDK, the agent tool surface — is generated from that schema rather than written against it.
The shim that passes review
The failure this prevents is subtle, which is why it is so common.
Someone needs one endpoint from a service. Generating a whole client feels heavy, so they write fifteen lines:
async function fetchInvoice(id: string): Promise<Invoice> {
const res = await fetch(`/api/invoices/${id}`);
return res.json() as Promise<Invoice>;
}This passes review. It is clear, it is small, and it works. It will keep working right up until someone renames a field on the server.
At that moment nothing breaks loudly. The Invoice interface still compiles — it is a hand-written assertion about a shape, not a check against one. The fetch still returns 200. The field is simply undefined, and the failure surfaces three layers away in a component rendering a blank where a total should be, or worse, in a report that is quietly wrong.
The shim has no link to the route it mirrors. Nothing connects the two, so nothing can tell you they have diverged.
Generate, then diff
The fix is to generate the client from the schema. That is the well-known half, and on its own it is not enough — because a generated file that is committed can be hand-edited, and a hand edit survives if nothing contradicts it.
So the generator gets teeth. CI regenerates every artefact and compares against what is committed. Any difference fails the build.
That single check converts the generator from advisory to authoritative. Without it, the sequence is predictable: someone tweaks a generated file to fix something urgent, the tweak works, nobody regenerates for a month, and by then the committed file and the schema disagree with no record of which is right. With the diff, that edit fails immediately, while the reason for it is still fresh.
The same check applies to anything else derived from a source of truth. A generated file says so at the top, names the command that regenerates it, and is diffed in CI. Without the diff, the first two are decoration.
Two layers, one of which is never touched
Generated code is rarely the whole story. Clients need auth, retries, a base URL, lifecycle handling — things no schema describes.
So each client is two layers: a generated half that nobody edits, and a thin hand-written layer on top of it. Auth interceptors, token refresh and convenience wrappers live in the hand-written layer. Request and response types live in the generated one.
The boundary is what makes the arrangement stable. When the schema changes, regeneration replaces the lower layer wholesale and the upper layer is untouched. When auth changes, the upper layer changes and no types move. Neither change threatens the other, which is precisely what fails when the two are interleaved in one hand-maintained file.
The wire has a shape, and it is not Python's
One more agreement worth making explicit, because it fails silently in a particularly annoying way.
Our wire format is camelCase. Our Python is snake_case. Rather than translating per field or per endpoint, every request and response model inherits a shared base that applies the conversion automatically and accepts either shape on input. TypeScript sends startDate, Python reads start_date, and no translation layer exists to fall out of sync.
Miss the shared base — inherit the plain model instead — and nothing raises. The server happily serialises start_date onto a wire where every client expects startDate. The client reads undefined from a field that is present under another name, and the bug looks like a missing value rather than a naming mismatch.
That one is caught by the drift check rather than a test, which is a reasonable division of labour: tests prove behaviour, the diff proves derivation.
What it buys
A schema change becomes mechanical. Change the endpoint, regenerate, and the type checker enumerates every call site that needs attention — before anything ships, in one pass, without anyone remembering which clients exist.
The alternative is not that this work disappears. It is that it happens later, one incident at a time, in whichever consumer nobody thought about.
Case Studies
Retail ERP and Point of Sale with Fiscal Integration
A complete single-store retail platform: stock ledger, supplier receipts, sales, and a point of sale that prints legally valid fiscal receipts on certified hardware.
Tyre Catalogue and Storefront Rebuild
Replacing an ageing off-the-shelf storefront with a typed catalogue platform: faceted search, an ingestion pipeline, and an admin surface that a non-developer can run.