Schema-First vs Code-First
Whether the contract file or the handler code comes first matters less than which one is the enforced source of truth. Schema-first buys review-before-build and cross-team parallelism; code-first buys iteration speed; drift — where the served API and the described API diverge — is the failure mode both must engineer away.
Frame the contract
API design starts with a consumer, a design question and a guarantee — never with a URL.
Two workflows, and what each optimizes
Schema-first: the contract file — OpenAPI: Describing the Contract, Not Designing It, protobuf, GraphQL SDL — is written and reviewed before implementation; server stubs, clients and docs are generated from it. The contract is designed as an artifact in its own right, which is precisely the posture this whole domain argues for: the review happens on the contract diff, *before* code exists to defend. The teams it pays for most are the ones with parallelism to unlock — consumer teams build against the agreed spec (with generated mocks) while the provider implements, and cross-team API review boards get a reviewable artifact instead of a walkthrough of handler code.
Code-first: handlers and types are written in the implementation language, and the spec is generated from annotations, decorators or reflection. It keeps one artifact in one language, gets full IDE and refactoring support, and iterates fastest — change the type, and the spec follows. Its gravity is the risk: when the contract is a byproduct of the code, contract *thinking* tends to become a byproduct too. The generated spec faithfully describes whatever the code happens to do — including the leaked ORM field and the accidentally-nullable response — and nobody reviewed those as promises (see Response Contracts Are Not Database Rows).
The dichotomy is softer than the debate: gRPC teams are schema-first by construction (gRPC: Schema, Codegen and Streams has no code-first option worth wanting); a two-person team iterating on an internal API is rationally code-first; and many mature teams land on code-first authoring with schema-first *governance* — the spec is generated, but it is committed, diffed and reviewed as the contract of record, and CI treats an unapproved spec change as a build failure.
Drift is the enemy, and it has two directions
Drift is the served API and the described API disagreeing, and each workflow drifts in its own direction. Schema-first drifts at the implementation seam: the spec says one thing, the handler does another — a field the stub scaffolded but the handler never populates, a documented 409 the code never returns, validation looser than the schema claims. Code-first drifts at the design seam: the spec is always accurate but the contract mutates without anyone deciding it should — an internal refactor renames a serialized field, and the generated spec dutifully documents the breaking change nobody chose (see Backward Compatibility: The Real Rules).
Both directions are closed mechanically, never by diligence. Against implementation drift: contract tests that exercise the real server and validate every response against the schema — request and response validation middleware in non-prod catches what the test suite misses (see Testing the Contract, Not Just the Code). Against design drift: the generated spec is committed to the repo, and CI diffs it against the approved version on every build — a changed contract fails the build until a human approves the diff, with a breaking-change classifier (OpenAPI: Describing the Contract, Not Designing It diff tooling) deciding which diffs need which level of sign-off. The committed-and-diffed spec is the single cheapest governance mechanism in this module: it converts every accidental contract change into a visible, reviewable event.
1PR #2114 "cleanup: rename internal DTO"2 3- class UserDto { displayName: string }4+ class UserDto { display_name: string }5 6# serializer follows the property name.7# generated spec updates itself. docs8# update themselves. everything is9# "accurate" — and every consumer10# reading displayName just broke.11# reviewer saw: an internal rename.1PR #2114 "cleanup: rename internal DTO"2 3CI: contract-diff FAILED4 openapi.json changed:5 BREAKING: response property6 'displayName' removed (GET /users/*)7 'display_name' added8 9# the same refactor now surfaces as10# what it is: a field removal, routed11# to the removing-fields process or12# reverted in 30 seconds.The code-first spec was never wrong — that is exactly the trap. Accuracy is not governance. Committing the generated spec and failing CI on unapproved diffs turns silent contract mutation into a reviewable decision.
Choosing: team shape decides, not ideology
The choice keys on three questions. Who reviews contract changes? If API review crosses team boundaries — platform boards, public-API governance — schema-first gives reviewers a native artifact; if the implementing team self-reviews, code-first with a gated generated spec delivers the same safety with less ceremony. Who builds in parallel? Consumer teams waiting on the provider make schema-first's design-before-build concretely valuable — mocks from the spec unblock them on day one. How fast does the contract churn? Early-stage APIs redesigning weekly suffer under schema-first's double bookkeeping (edit spec, regenerate, reconcile); stable public contracts amortize it trivially.
Wherever you land, two invariants are non-negotiable, and they matter more than the direction. First: one source of truth, declared — a repo where some endpoints are spec-generated and others hand-annotated, with nobody sure which, is worse than either pure workflow. Second: the schema is not the contract — it captures shapes and operations, while idempotency, ordering, consistency and unknown-value rules live in prose the schema cannot express (see OpenAPI: Describing the Contract, Not Designing It and Documentation Is Part of the Contract); whichever artifact comes first, those clauses still have to be written by someone who means them.
| Situation | Fit | Why |
|---|---|---|
| Public API, cross-team review board | Schema-first | The contract diff is the unit of governance; reviewers never read handler code |
| Consumer teams blocked on provider build | Schema-first | Agreed spec + generated mocks unlock parallel integration work |
| Internal API, one team, high churn | Code-first + committed spec gate | Iteration speed wins; the CI diff still catches accidental breaks |
| gRPC / protobuf shop | Schema-first (by construction) | The .proto is the only workable source of truth; codegen is the workflow (see gRPC: Schema, Codegen and Streams) |
| Legacy API, no spec at all | Generate, commit, gate — then improve | A generated spec of the real behavior is the baseline every later decision diffs against |
Key points
- The real question is not which comes first but which artifact is the enforced source of truth — and what mechanically stops the served API from drifting off it.
- Schema-first buys contract review before code exists and unlocks parallel consumer builds; code-first buys iteration speed and single-language ergonomics.
- Schema-first drifts at the implementation seam (spec says, code does); code-first drifts at the design seam (spec accurately documents changes nobody decided).
- Close both mechanically: schema-validated contract tests against the real server, and a committed spec diffed in CI with breaking-change classification.
- Code-first authoring with schema-first governance — generated, committed, gated — is a legitimate and common landing point.
- The schema is not the contract: idempotency, ordering, consistency and unknown-value rules live in prose either way.
Follow the failure
How the contract fails or gets misused, hop by hop — and what it costs when it completes.
- 1Team → workflow: adopts code-first for speed; the spec is generated on deploy, published to the docs site, committed nowhere.
- 2Team → refactor: an internal DTO rename changes a serialized field name; types, tests and generated docs all update consistently.
- 3Review → PR: the reviewer sees an internal cleanup; no artifact in the diff says "contract change", so nobody asks the compatibility question.
- 4Consumers → production: clients reading the old field break; the docs are examined and found accurate — as of five minutes after the deploy.
- 5Team → process: adds a manual "check the docs diff" review step; it holds for six weeks, until the next urgent PR — vigilance was never the missing piece, the CI gate was.
- Silent contract mutations ship as refactors: accurate-but-ungoverned specs mean breaking changes arrive with green builds and clean reviews.
- Consumers integrate against a spec the server does not honor (implementation drift): generated clients fail on undocumented nulls and absent fields, and trust in the spec — the whole point of having one — dies.
- Mixed-truth repos (some endpoints spec-driven, some code-driven) make every contract question archaeological: the answer depends on which endpoint you ask about.
Design, observe, evolve
A contract decision is incomplete until you know how you would notice it failing and how it changes later.
- • Declare the source of truth in writing — spec or code — and make the other artifact generated, never hand-edited; delete any third copy.
- • Commit the contract file to the repo and gate CI on its diff: unapproved changes fail the build, and a classifier routes breaking diffs to the [[backward-compatibility]] process.
- • Run schema-validated contract tests against the real server in CI, and response-validation middleware in staging, so implementation drift is caught before consumers find it.
- • Keep the prose clauses (idempotency, ordering, extensibility rules) versioned next to the schema, so a contract review sees the whole contract, not just the shapes.
- • Track contract-diff events per release — how many were flagged, how many were breaking, how many were overridden; a rising override rate means the gate is becoming theater.
- • In staging, alert on schema-validation failures from response middleware: each one is implementation drift caught in the act.
- • Watch consumer-reported "docs are wrong" tickets: with an enforced source of truth they should trend to zero, and any residue points at the prose clauses the schema cannot check.
- • A committed, diffed spec is the substrate for every evolution practice in this module: [[deprecation]] annotations, [[removing-fields]] tracking and changelog generation all hang off the artifact you now control.
- • Workflow can migrate: legacy code-first APIs adopt governance by committing today's generated spec as the baseline — no rewrite required, the gate starts working immediately.
- • Generated SDKs, mocks and docs regenerate on every approved contract change, which turns [[sdk-design]] and [[api-documentation]] freshness from a chore into a build step.
- • Schema-first is double bookkeeping under churn: every experiment edits the spec, regenerates, and reconciles — real friction exactly when the design is least settled.
- • Codegen pipelines are infrastructure with sharp edges: generator version drift, custom-type mappings and template quirks become build problems the team must own.
- • CI contract gates add latency to every PR that touches the surface, and the override path must be cheap enough that engineers do not learn to route around the gate entirely.