APIs
OpenAPI: making the contract the source of truth, not the docs
A backend engineer adds a tenantId field to the create-order payload and marks it required — a one-line change, shipped Friday. By Monday, three mobile clients, a partner integration, and the internal admin app are all returning 422s on every order. None of them sent tenantId; none of them knew it existed. The contract said the field was optional yesterday and required today, and nothing in the pipeline noticed. The “docs” were a Confluence page last edited six months ago. The real contract lived in someone’s head, and it changed without telling anyone.
The spec is a contract or it is fiction
OpenAPI is a machine-readable description of your HTTP API: every path, every request and response shape, every status code, every auth scheme, written in YAML or JSON. That much most teams have. The question that decides whether it helps or hurts is one level deeper: is the spec the source of truth, or a description of it written somewhere else?
When the spec is a side document — written once, exported to a docs portal, never checked against the running service — it rots within months. Code changes, the spec doesn’t, and now you have two contracts: the one the server enforces and the one the spec claims. Clients integrate against the spec, the spec is fiction, and the integration breaks in production. This is contract drift, and it is the default outcome unless something actively prevents it.
The fix is not “write better docs.” It is to make the spec load-bearing: code is generated from it, requests are validated against it, and a diff of it gates every pull request. A contract nobody can violate without the build failing is a contract. Everything else is a wiki page.
Spec-first vs code-first: where the truth lives
There are two ways to get a spec, and the difference is which artifact is authoritative.
Spec-first (design-first): you author the OpenAPI document by hand, review it like an API design, and generate server stubs, typed clients, and mock servers from it. The spec is the source of truth; code conforms to it. Frontend can build against a generated mock the same day backend starts, because the contract exists before either implementation does.
Code-first: you write the server with annotations or decorators (@ApiProperty, FastAPI type hints, springdoc), and a tool generates the spec from the running code. The code is the source of truth; the spec is a reflection of it. Less ceremony, and the spec can’t drift from the handler it was extracted from — but it can drift from what consumers expected, because nobody reviewed the contract as a contract before shipping.
Neither is wrong. The senior call is: spec-first when multiple teams or external partners consume the API and the contract needs review and stability; code-first when one team owns both ends and velocity matters more than upfront design. What’s not optional in either model is enforcement — the spec must be validated and diffed regardless of how it was born.
| Dimension | Spec-first | Code-first |
|---|---|---|
| Source of truth | The hand-written spec | The server code + annotations |
| Contract reviewed before code? | Yes — it’s a design artifact | No — emerges from implementation |
| Parallel client/server work | Day one (generate a mock) | After the server compiles |
| Drift risk | Spec vs impl (validate at edge) | Contract vs consumer expectation |
| Best fit | Public APIs, many consumers | One team owns both ends |
What OpenAPI 3.1 actually buys you
A spec earns its keep through the tooling it unlocks, not the document itself. Four payoffs matter:
- Generated typed clients and server stubs. Run a generator over the spec and get an SDK in TypeScript, Go, Swift, Kotlin — methods, models, error types included. The integration class of bug (“I sent
userId, the server wanteduser_id”) disappears, because nobody hand-writes the request anymore. Teams shipping public APIs generate SDKs from the spec precisely to kill that whole category. - Request validation at the edge. A middleware validates incoming requests against the spec before they reach your handler. A malformed body is rejected with a precise 400 that names the offending field, and your business logic never sees garbage.
- Docs that can’t silently drift. When the rendered docs are generated from the same spec that gates CI, “the docs are wrong” stops being possible — the docs are the contract.
- Mock servers. A tool serves fake responses straight from the spec’s examples, so consumers integrate before the backend exists.
OpenAPI 3.1 specifically aligns the schema language with JSON Schema 2020-12, so your validation keywords are real JSON Schema, not a 3.0-era subset. The cost of that power: nullable: true is gone — you now write type: ["string", "null"] — and exclusiveMinimum/exclusiveMaximum take a numeric value, not a boolean. 3.1 also adds a top-level webhooks object for documenting events your API sends, not just receives. Many tools still default to 3.0, so check generator support before you upgrade.
Why this works
Code generators are not magic — they encode opinions. A spec with loose schemas (additionalProperties: true, missing required arrays, untyped object blobs) generates loose, any-riddled clients that defeat the point. The discipline that makes generation valuable is tight schemas: explicit types, explicit required, reused components. The generator is only as honest as the contract you feed it.
Breaking-change detection: the gate that catches the Friday deploy
The tenantId story has one clean fix: a tool that diffs the new spec against the previous version and fails the PR on a breaking change. oasdiff is the common choice — it classifies 450+ change categories and knows which are breaking. Adding a required request field, removing a response field a client might read, narrowing a type, removing an enum value, deleting an endpoint: all breaking, all caught before merge.
Wired into CI, the check is blunt and effective: the pipeline diffs the PR’s spec against main, and if a breaking change appears, the build is red and the engineer sees exactly which field broke before any client ever does. The Friday deploy never reaches production, because the contract is enforced by the same gate as the tests. A subtlety with 3.1: because nullability moved into the type array, removing "null" from type: ["string", "null"] registers as a type change rather than a nullable change — the diff still catches it, just under a different rule, which is why you check the category the tool reports, not just the count.
A public API is consumed by mobile apps you can't force-update and three partner integrations. Pick the enforcement strategy.
A PR adds a new required field to a request body. What should a healthy API pipeline do?
Your hand-written docs say a field is optional, but the server now rejects requests without it. What is this called and what prevents it?
Order the steps of a spec-first pipeline that prevents drift:
- 1 Author / update the OpenAPI spec and review it as a contract
- 2 CI diffs the spec against the previous version (oasdiff) and fails on a breaking change
- 3 Generate typed clients, server stubs, and a mock server from the spec
- 4 Validate incoming requests against the spec at the edge before the handler runs
- 5 Render docs from the same spec so they can never contradict the contract
- 01A teammate says 'we already have an OpenAPI spec, so we're covered against breaking changes.' Why is that not true, and what's actually missing?
- 02When would you choose code-first over spec-first, and what enforcement do you still need either way?
OpenAPI is a machine-readable description of an HTTP API, but the description is worthless unless it is the source of truth. The failure mode is contract drift: hand-written docs say one thing, the server enforces another, clients integrate against the fiction, and a one-line change like adding a required field breaks every consumer in production. Spec-first treats the hand-written spec as authoritative and generates clients, stubs, and mocks from it; code-first generates the spec from annotations. Either way, the spec must be enforced — generated typed clients kill the whole class of hand-rolled-request bugs, edge validation rejects malformed input before your handler, and a breaking-change diff (oasdiff) fails the PR the moment someone removes a field, narrows a type, or adds a required one. OpenAPI 3.1 aligns schemas with JSON Schema 2020-12, drops nullable for type arrays, and adds webhooks. Tighten your schemas with reused $ref components, explicit required, and security schemes, then let the pipeline make the contract impossible to break silently.