Code-first and spec-first OpenAPI workflows fail in different ways — and neither prevents contract drift unless linting and contract tests are actually wired into CI.
A backend team adds a required field to an endpoint's request body. They update the handler, add validation, ship it, tests pass. Three weeks later, a partner integration starts failing with cryptic 400s. The partner's SDK — generated months earlier from the team's published OpenAPI spec — never got the memo, because nobody regenerated the spec from the code. The code was right. The contract was stale. Every consumer that trusted the contract was now wrong, silently, until someone hit the endpoint and got an error that made no sense against the docs they were reading.
This is the failure mode that "spec-driven development" exists to prevent, and it's worth being precise about what that phrase actually means — because "we use OpenAPI" and "we do spec-driven development" are not the same claim. A lot of teams have a swagger.json file. Far fewer treat it as the actual source of truth.
There are two ways an OpenAPI document comes into existence, and they produce different failure modes.
Code-first (annotation-driven). You write the route handlers, decorate them with annotations or decorators (@ApiOperation, docstrings, or comments consumed by a tool like swagger-jsdoc, FastAPI's automatic schema generation, or springdoc-openapi), and a generator walks the code to produce the spec. The code is definitionally the source of truth, because the spec is derived from it. This is fast to start and never drifts by definition — but it also means the spec quality is only as good as the annotations, and API design decisions get made implicitly, one endpoint at a time, as code is written. There's no artifact to review before implementation exists.
Spec-first (contract-driven). You write the OpenAPI YAML by hand (or via a design tool) before implementation. Backend and frontend teams review and agree on the contract — request/response shapes, status codes, error formats — as a design document. Once it's approved, backend builds against it, frontend can start building against a mock server generated from the same spec (tools like Prism spin up a mock API directly from an OpenAPI file), and client SDKs get generated for multiple languages from one file. The spec is the thing everyone builds toward, not a description of what already exists.
The catch with spec-first is exactly what happened in the example above: nothing stops the implementation from drifting away from the spec once both exist independently. Spec-first buys you upfront design clarity and parallel workstreams; it does not automatically buy you consistency over time. That has to be enforced.
| Code-first | Spec-first | |
|---|---|---|
| Source of truth | The code | The YAML/JSON contract |
| API design review | Happens implicitly, per PR | Happens explicitly, before code exists |
| Parallel frontend/backend work | Limited — frontend waits on real endpoints | Full — frontend mocks against the spec |
| Client SDK generation | Possible, but spec quality varies | Reliable, spec is authoritative |
| Drift risk | Low (spec is generated from truth) | High, unless enforced in CI |
| Best fit | Small teams, internal APIs, fast iteration | Public APIs, multiple consumer teams, versioned contracts |
Most teams that say "we do spec-first" actually run a hybrid: spec-first for the initial design, then code-first-in-practice because nobody wired up enforcement, and the spec quietly becomes documentation-shaped fiction. That hybrid is fine if you know that's what you're doing — the failure is treating a stale spec as if it were still authoritative.
Writing the spec is the easy 20%. The part that prevents drift is treating the OpenAPI document like any other artifact with tests:
Linting. Tools like Spectral check the spec itself for structural and style problems — missing descriptions, inconsistent naming, undefined error responses — before it's even implemented. This catches design mistakes early, the same way ESLint catches code mistakes early.
Contract testing. Tools like Dredd or Schemathesis run the live API against its own OpenAPI spec and fail CI if a response doesn't match the declared schema. This is the piece most teams skip, and it's the piece that would have caught the stale-required-field bug: if the spec says a field is optional and the implementation actually requires it (or vice versa), contract tests catch the mismatch on every PR, not three weeks later in a partner's error logs.
Breaking-change detection. Tools like openapi-diff or oasdiff compare two versions of a spec and flag changes that break backward compatibility — a field going from optional to required, a response type narrowing, an endpoint disappearing. Wiring this into CI on any spec change turns "did we just break someone's integration" from a code-review guess into an automated check.
A minimal paths entry that a linter and a contract test both care about:
/orders/{orderId}:
get:
operationId: getOrder
parameters:
- name: orderId
in: path
required: true
schema: { type: string, format: uuid }
responses:
'200':
description: Order found
content:
application/json:
schema:
type: object
required: [id, status, total]
properties:
id: { type: string, format: uuid }
status: { type: string, enum: [pending, shipped, delivered] }
total: { type: number }
'404':
description: Order not found
Note what's doing the real work here: required: [id, status, total] and the enum on status. Those aren't documentation flourishes — they're the assertions a contract test checks the live response against. A handler that starts returning status: "canceled" without updating this enum is a contract violation, and if nothing runs this check in CI, it's a violation nobody will notice until a client's deserializer throws.
Teams often reduce API versioning to a URL prefix (/v1/, /v2/) and stop there. But the spec is where versioning actually needs discipline: $ref-shared schemas mean a change to a common Address object silently ripples into every endpoint that references it, across every version if the versions aren't cleanly forked. Practically, that means either maintaining fully separate spec files per major version, or being rigorous about only ever making additive (backward-compatible) changes to shared components and forking a component the moment a version needs to change its shape. Breaking-change detection tooling matters most exactly here, since a shared $ref change is the easiest way to break three endpoints while intending to fix one.
Spec-first design is valuable mainly for the upfront review it forces and the parallel work it unlocks — not because writing YAML before code is inherently virtuous. The value degrades to zero the moment the spec and the implementation are allowed to diverge unchecked, which is the default outcome unless linting and contract tests are actually wired into CI. If you're evaluating whether a team's "OpenAPI-driven" process is real: ask not whether a spec file exists, but whether a PR that violates it fails a build. If you just need to explore or sanity-check an existing spec's structure and endpoints without standing up tooling, Utilix's OpenAPI Viewer will browse and search one directly in the browser.