- API Design
- Type Safety
- Backend
Typed contracts between backend and client: why shared schemas cut integration bugs
"It worked in Postman" is a sentence nearly every team has said at least once, right before discovering the real problem was somewhere else. A typed contract between backend and client moves an entire class of bugs from runtime discovery to compile-time error.
9 min de leitura

"It worked in Postman" is one of the most familiar sentences on any team that develops its backend and client separately. Postman sends a manual request, gets back a response, and a developer eyeballs it and says "yes, that's correct." The problem isn't that Postman lies; the problem is that this confirmation is only about that one specific request, not about the contract the real client actually depends on — assumptions about each field's type, whether it's optional or required, and the exact shape of an array or a nested object.
Where "it worked in Postman" breaks
The gap between "a sample response a developer's eye approved" and "the contract the client actually relies on" is exactly where most integration bugs live. A field present in 99% of responses but null or absent in one particular case (a user who hasn't finished their profile, say). A number the backend returns as a string because of a default serialization library, while the client assumed it was a number. An array that comes back as null instead of [] when empty. None of these show up in a manual Postman test against typical sample data, because sample data is usually the happy path, not the edge case.
The deeper problem is that this class of error usually reveals itself at runtime rather than at development time — precisely when fixing it is most expensive: after deployment, once a real user has hit that edge case. A shared typed contract moves this entire class of error from the "runtime bug a user discovers" bucket into the "compile-time error caught before merge" bucket. That difference is the difference between an hour of development time and an hour of debugging at two in the morning.
Generated types vs. hand-written types
Once a team decides to address this, it usually chooses between two approaches: hand-write client types and keep them in sync with API documentation, or generate types automatically from a single source of truth (a backend schema definition, say).
Hand-written types get you started faster and are perfectly reasonable for a small, stable API. But their cost shows up over time, not on day one: every backend change requires someone to remember that the client's hand-written types need the same change applied manually, and "remembering" is exactly what human processes are bad at. Generated types remove that manual-reminder loop entirely: client types come directly from the same definition the backend follows, and if the backend changes, regenerating the types immediately shows where client code no longer matches the new contract — not months later, but the moment generation runs or the build compiles.
The cost of generated types lives elsewhere: it requires a generation tool, an extra step in the build process, and the discipline that nobody ever hand-edits a generated type (because the next time generation runs, that manual edit silently disappears). For small teams with a very simple API, this cost may outweigh the benefit. For a team whose API is growing, with more than one person working across backend and client, the general industry experience is that the break-even point arrives much sooner than it seems like it should.
Contract changes across a release boundary
Where this stops being a stylistic preference and becomes a genuine correctness problem is the release boundary. Backend and client almost never deploy at exactly the same moment — a mobile app can take days or weeks for all users to update, while a backend can deploy several times a day. That means there's always a window where an old client version talks to a new backend version, or the reverse.
A typed contract alone doesn't solve this; it only makes it visible. The real responsibility rests on compatibility rules the team has to choose deliberately: a newly added field must be optional so an old client that doesn't know about it doesn't break; a field that's no longer used shouldn't simply be removed, but marked deprecated and kept alongside its replacement for a defined period; and changing an existing field's type (from string to number, say) should almost always be modeled as an entirely new field, not an in-place change. These are API versioning rules, and the typed contract is just the tool that makes them enforceable and reviewable — without that tool, the same rules still have to be followed, but a violation goes unnoticed until a real user hits it.
Contract testing
The final layer that completes this picture is contract testing. An ordinary backend unit test confirms the service's internal logic works correctly; an end-to-end test confirms the whole system works correctly in a simulated environment. Neither directly answers the question that actually matters here: "if the client version currently in users' hands talks to the next backend version about to be deployed, what happens?"
Contract testing fills exactly that gap: the consumer (the client) documents its expectations of the contract in an executable form, and the producer (the backend) runs those expectations as part of its own build process before deploying any change. This differs from generated types: types guarantee the shape of data is correct at compile time, but they can't guarantee the service's actual behavior (say, "this field is always populated in this particular case") is still the behavior the client is counting on. Contract testing covers that behavioral layer, where types alone cannot.
Which layer actually owns the schema
A question teams usually raise too late is: where does the schema actually start? Two patterns are common. In the first, the backend owns the schema — route, input and output definitions live in backend code, and client types are generated from that. In the second, an independent schema definition (a contract file that belongs to neither the backend nor the client) is the source of truth, and both backend and client are generated from it.
The first pattern is simpler and entirely sufficient for a team with one backend and one or two clients, because there's only one direction of generation and no need to coordinate between two independent codebases. The second pattern earns its value once there's more than one backend team, or once the same contract has to be consumed by different programming languages (one backend language and several client languages, say). Picking the wrong pattern usually shows up as: a team that adopted the second pattern for a single backend and a simple client ends up spending more time maintaining a separate contract file than doing the work it's actually paid for.
Adopting this gradually on an old API
Most teams don't make this decision on a greenfield project; they make it on a years-old API that was written without a typed contract from the start and now has hundreds of endpoints. A full one-shot rewrite is almost always impractical and usually never finishes. What works in practice is starting with the highest-traffic or highest-incident endpoints — the ones with the longest history of integration bugs — and adding a typed contract endpoint by endpoint, while the rest of the API stays untyped until its turn comes. That means both approaches coexist in the same codebase for a while, and that state needs to be accepted deliberately, not as a temporary mess to be "cleaned up later," but as the actual migration path.
A sign that this migration is going well is that each newly typed endpoint immediately surfaces one or two old, silent bugs of its own — a field that was always assumed required but wasn't in one particular case, or a type the documentation described one way while the real response returned something else. That surfacing is itself a good reason to keep the migration going rather than stopping after the first few endpoints.
The practical takeaway
None of these tools — shared types, automatic generation, contract testing — replace good API design. They only shorten the time between "the contract broke" and "someone noticed." For a team that deploys its backend and client separately, at different speeds, that shortened gap is exactly what makes the difference between a quiet release and a week full of support tickets.
Mais leituras

- Vector Search
- Retrieval
Vector retrieval in production: when a vector database earns its cost
A vector database is an architectural decision, not an automatic upgrade to search. If you can't name exactly what you're missing from plain text search, you probably don't need o…
8 min de leitura
- Trust
- News Systems
Scoring news trustworthiness: designing a system that doesn't claim to be neutral
A trust label is an editorial decision encoded in software, not a measurement. Every design choice downstream of that fact — from data model to what you show the reader — depends…
9 min de leitura
- AI Infrastructure
- Model Routing
One model is a single point of failure: routing between hosted and local AI models
Wiring a feature to a single model provider looks like the simple choice — until that provider gets slow, changes price, or fails on one kind of input. Routing across models isn't…
8 min de leitura
Tem algo para construir?
Diga-nos em que está a trabalhar. Dizemos-lhe com honestidade se somos a equipa certa para isso.
ou escreva-nos para hello@larsima.com
