Evolutionschema-firstcode-firstdriftsource of truthgovernance

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.

Follow the failure

Frame the contract

API design starts with a consumer, a design question and a guarantee — never with a URL.

Design question
Which artifact is the source of truth for this contract, and what mechanically prevents the served API from drifting away from it?
Consumers
Everyone who builds against the description instead of the wire: consumer teams starting integration before the server exists, SDK generators, contract-test suites, the reviewer approving a change by reading a diff — and CI, the only consumer that never gets tired of checking.
The promise
A declared and enforced source of truth guarantees that what the spec says, the server serves — so reviews, generated clients and consumer expectations are all statements about the same API, not three approximations of it.
RequirementConsumersResource ModelStyleContractValidationAuthorizationErrorsIdempotencyPaginationVersioningObservabilityEvolutionTrade-offs

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.

schema-firststubscode-firstreviewed as contractContract design / reviewCode + annotationsSchema (spec file)Generated specServer implementationGenerated: clients · mocks · docsCI: diff vs approved contract
UserLLMAgentToolDataDecisionHumanGuardrail

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.

Code-first with no gate: the refactor that was secretly a breaking change
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. docs
8# update themselves. everything is
9# "accurate" — and every consumer
10# reading displayName just broke.
11# reviewer saw: an internal rename.
Same PR, with the spec committed and gated
1PR #2114 "cleanup: rename internal DTO"
2
3CI: contract-diff FAILED
4 openapi.json changed:
5 BREAKING: response property
6 'displayName' removed (GET /users/*)
7 'display_name' added
8
9# the same refactor now surfaces as
10# what it is: a field removal, routed
11# to the removing-fields process or
12# 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.

The decision, by team shape
SituationFitWhy
Public API, cross-team review boardSchema-firstThe contract diff is the unit of governance; reviewers never read handler code
Consumer teams blocked on provider buildSchema-firstAgreed spec + generated mocks unlock parallel integration work
Internal API, one team, high churnCode-first + committed spec gateIteration speed wins; the CI diff still catches accidental breaks
gRPC / protobuf shopSchema-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 allGenerate, commit, gate — then improveA 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.

  1. 1
    Team → workflow: adopts code-first for speed; the spec is generated on deploy, published to the docs site, committed nowhere.
  2. 2
    Team → refactor: an internal DTO rename changes a serialized field name; types, tests and generated docs all update consistently.
  3. 3
    Review → PR: the reviewer sees an internal cleanup; no artifact in the diff says "contract change", so nobody asks the compatibility question.
  4. 4
    Consumers → production: clients reading the old field break; the docs are examined and found accurate — as of five minutes after the deploy.
  5. 5
    Team → 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.
What breaks
  • 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.

Design the contract
  • • 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.
Observe in production
  • • 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.
Evolve without breaking
  • • 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.
What it costs
  • • 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.

Misconceptions

Claim
“Code-first means the spec is always accurate, so drift is a schema-first problem.”
Reality
Code-first specs are accurate and ungoverned — they faithfully document contract changes nobody decided to make. Accuracy without a diff gate is how breaking changes ship with green builds.
Claim
“Schema-first guarantees the server matches the spec.”
Reality
Generation constrains the scaffolding, not the behavior. Handlers return undocumented nulls, skip documented errors and under-validate regardless of what generated their stubs; only contract tests against the running server close that seam.
Claim
“Choosing schema-first is the same as choosing OpenAPI.”
Reality
Schema-first is a workflow; OpenAPI is one schema language for one style of API. Protobuf and GraphQL SDL teams are schema-first too — and all three schemas still cannot express the behavioral clauses that live in prose (see OpenAPI: Describing the Contract, Not Designing It).