The idea
In our platform, a response shape is defined once, by the service that produces it. Everyone else takes that definition by reference or generates code from it. Nothing is copied by hand, and the build checks each link.
It sounds strict. In practice it removes a whole class of arguments, because the contract file is the answer.
How it fits together
- Every service starts from an OpenAPI file. Its controllers implement an interface generated from that file, so an endpoint cannot exist without being in the contract.
- The web backend-for-frontend does not redefine a response that a service already owns. It refers to the owner's schema, so there is no second copy that can drift.
- The apps generate their types from the bundled contract. A hand-written type that mirrors an API response is treated as a mistake.
- Each service's tests validate its real responses against its own spec, so the spec cannot quietly lie about the code.
What the build checks
On every change, the specs are linted for style and for one specific rule: no response body under the public API may be an untyped free-form object. The split specs are bundled into one file for the clients. And a tool compares the new contract against both the base branch and the most recent released snapshot, and fails the build on a breaking change.
Why the released snapshot matters
A web page can be fixed in minutes. A phone cannot be forced to update. Someone is always running last year's app. So breaking changes are measured against what has actually shipped to people, not just against the last commit.
That is why inside a version of the API we only add. Removing or narrowing something an old build reads is a breaking change. If something has to go, we mark it deprecated, send a sunset header, keep it for the support window, and remove it only in the next major version. The clients, in turn, are tolerant readers: they ignore fields they do not know, so an addition never breaks them.
Where the contract has to bend
Web and mobile sometimes need different answers. Our web client keeps its session in a cookie, while the mobile app receives tokens in the response body. We model that in the contract as two named variants that the operation can return, rather than leaving one loose shape that fits neither. A test keeps the shared fields in the two variants identical.
A few changes are compatible on the wire but still look breaking to the comparison tool, for example turning one schema into a union. Each of those is recorded, finding by finding, in a reviewed list, so the check stays strict without becoming noise.