OpenAPI: Describing the Contract, Not Designing It
OpenAPI captures paths, operations, schemas and security schemes in a machine-readable file — which earns you linting, diffing, mocks, generated clients and always-current reference docs. What it cannot capture is most of what this domain teaches: guarantees live in prose, and the spec is the skeleton they hang on.
Frame the contract
API design starts with a consumer, a design question and a guarantee — never with a URL.
What the file actually holds
An OpenAPI document is four registries in one file. Paths and operations: every URL template and method, with parameters, request bodies and per-status responses. Schemas: the shape vocabulary — types, required-vs-optional, nullability, enums, constraints — shared across operations via $ref, which is where naming discipline pays (one Order schema referenced everywhere beats five slightly different inline ones — see One Vocabulary: Naming and Consistency). Security schemes: how callers authenticate — API key, OAuth2 flows with scopes, bearer — declared once, applied per operation (see Authentication in the Contract and Scopes: Least Privilege as Contract Surface). Metadata: servers, versions, deprecation flags, and examples that double as documentation and mock data.
Read a fragment the way the tooling does: every line below is machine-checkable. A linter can demand every operation have a 4xx response; a diff tool can classify required: [amount, currency] gaining a member as breaking; a mock server can serve the example verbatim; a generator can emit a typed createPayment() from it. That checkability is the entire value proposition — the same facts in a wiki page can do none of this.
paths:
/payments:
post:
operationId: createPayment
security: [{ apiKey: [] }]
parameters:
- name: Idempotency-Key
in: header
required: true # checkable: linter, validator
requestBody:
content:
application/json:
schema: { $ref: '#/components/schemas/PaymentCreate' }
responses:
'201': { $ref: '#/components/responses/Payment' }
'409': { $ref: '#/components/responses/IdempotencyConflict' }
'422': { $ref: '#/components/responses/ValidationError' }
components:
schemas:
PaymentCreate:
type: object
required: [amount, currency] # diff tool: adding here = breaking
properties:
amount: { type: integer, minimum: 1 } # cents — SAYS WHO? prose.
currency: { type: string, pattern: '^[A-Z]{3}$' }The tooling dividend — and the file that must be true
Each consumer of the file converts it into leverage, and together they are why the spec is worth maintaining at all. But every one of them assumes the file matches the server — a wrong spec is worse than none, because generated clients, mocks and diff verdicts all inherit the lie with full confidence. The file must therefore be governed: one source of truth, committed, diffed in CI, verified against the running server by contract tests (the whole apparatus of Schema-First vs Code-First — this lesson is why that one matters).
- Linting — style rules enforced mechanically: every operation has error responses, every schema field has a description, pagination parameters follow the house convention (see Design Principles Without Commandments). The review checklist becomes CI.
- Breaking-change diffing — the spec diff classified against the Backward Compatibility: The Real Rules lists: removed field, newly-required parameter, narrowed enum → build fails pending explicit approval. The single highest-value tool in the chain.
- Mock servers — consumers integrate against spec-served examples weeks before the real server exists; the parallelism dividend of schema-first work.
- Generated clients and server stubs — typed SDK surface from the file (see SDK Design: The Contract's User Interface); the generator's output quality is capped by the spec's schema discipline.
- Request/response validation — middleware validating live traffic against the spec in staging: implementation drift caught as validation errors, not consumer bug reports (see Testing the Contract, Not Just the Code).
- Reference docs — rendered directly from the file, so the reference section of Documentation Is Part of the Contract is current by construction.
- Agent consumption — LLM-based callers select and invoke operations from the spec's descriptions;
operationId, parameter descriptions and examples are now runtime behavior, not documentation garnish.
What the spec cannot say — and the two failure modes
Now the boundary. OpenAPI describes *shapes*; this domain's hardest-won content is *behavior*, and behavior does not fit the schema. Whether that documented Idempotency-Key replays the original response or errors on reuse (Idempotency Keys: The Mechanism); whether a 201 means the resource is readable on the next GET (Consistency as a Contract Clause); whether list order is stable, whether the enum is open (Enum Evolution: The New Value That Broke Old Clients), what amount: integer is *denominated in*; retry guidance, rate-limit semantics (The Rate-Limit Contract), pagination cursor lifetimes. Every one of these lives in description fields and prose — which machines skip and humans must write. The schema minimum: 1 above is checkable; "amount is in minor currency units" is a sentence someone has to mean.
Hence the two failure modes, mirror images of each other. Spec-worship treats the rendered spec as complete documentation — shapes without guarantees — and ships an API whose behavioral contract is undefined exactly where it matters most; the fix is treating the spec as the skeleton of Documentation Is Part of the Contract, never the body. Spec-driven design goes wronger earlier: designing the API *inside* what OpenAPI can express, so operations become anemic CRUD-over-schemas because richer semantics — long-running operations (Long-Running Operations: 202 and the Job Resource), event streams (Server-Sent Events), explicit state machines (Resources Have State Machines) — are awkward to describe. The design comes first, from requirements and consumers; the description follows. An API contorted to fit its description format has the relationship exactly backward.
| Contract clause | In the schema? | Where it actually lives |
|---|---|---|
| Field types, optionality, nullability, enum values | Yes — fully checkable | Schemas; diffed and validated mechanically |
| Which operations exist, with which parameters and statuses | Yes | Paths; linted and diffed |
| Auth scheme and required scopes per operation | Yes | Security schemes (see Scopes: Least Privilege as Contract Surface) |
| Idempotency behavior, retry safety, replay semantics | No | Prose: operation descriptions + Documentation Is Part of the Contract |
| Consistency (read-after-write), ordering guarantees | No | Prose, deliberately written (see Consistency as a Contract Clause) |
| Units, meanings, open-vs-closed enum policy | No — integer is not cents | Field descriptions; the Enum Evolution: The New Value That Broke Old Clients clause |
| Rate limits, quotas, deprecation timelines | Partially (extensions, deprecated: flag) | Headers + prose + Deprecation as a Process, Not a Label process |
Key points
- OpenAPI is four registries — paths/operations, schemas, security schemes, metadata — whose value is machine-checkability: lint, diff, mock, generate, validate from one file.
- The breaking-change diff in CI is the highest-value tool in the chain: it mechanizes half the Backward Compatibility: The Real Rules lists.
- Every tool inherits the file's truthfulness; an ungoverned spec poisons clients, mocks and diff verdicts with confident lies — governance comes first.
- The spec captures shapes, not behavior: idempotency, consistency, ordering, units and enum-openness live in prose that humans must write and machines will not check.
- Never design inside the description format: requirements and consumers shape the API, and the spec describes the result — not the reverse.
- Descriptions and examples are load-bearing: doc renderers, mock servers and agent callers all execute them.
Follow the failure
How the contract fails or gets misused, hop by hop — and what it costs when it completes.
- 1Team → spec: publishes a generated OpenAPI file to the docs portal; description fields are empty, examples are placeholder lorem-JSON.
- 2Consumers → spec: generate clients and integrate against the shapes; every behavioral question (retry? replay? readable-after-create?) is answered by experiment.
- 3Spec → reality: nothing verifies the file against the server; a nullable field the spec calls required starts failing generated clients in production.
- 4Team → design: the next feature — a long-running export — is forced into synchronous CRUD shape "so it fits the spec tooling"; the honest The Async Job Pattern contract never gets considered.
- 5Consumers → trust: integrators learn the spec is aspirational, hand-write clients against observed behavior, and the tooling dividend evaporates for everyone.
- Generated clients fail at runtime on spec/server mismatches — nullability and required-ness lies surface as deserialization crashes in consumer code, attributed first to the consumer.
- Behavioral gaps ship as consumer incidents: the spec was silent on idempotency and replay, so every consumer guessed, and the guesses disagree (see Idempotency: Surviving the Retry).
- Design bends toward the describable: APIs flatten into anemic CRUD because richer contracts are awkward in the format, and the flattening is permanent.
Design, observe, evolve
A contract decision is incomplete until you know how you would notice it failing and how it changes later.
- • Govern the file before wielding it: one declared source of truth, committed, CI-diffed with breaking-change classification, and contract-tested against the running server (see [[schema-first-vs-code-first]]).
- • Lint for completeness, not just style: every operation carries error responses, every field a description with units and semantics, every enum an explicit extensibility note.
- • Write the behavioral clauses into `description` fields and the surrounding docs as deliberately as the schemas — idempotency, consistency, ordering, retry guidance per operation.
- • Design the API from requirements first and describe it second; where the format resists the honest design (async jobs, streams), keep the design and describe it as best the format allows.
- • Spec-vs-server validation failures in staging middleware are the drift alarm; each one is a lie in the file caught before a consumer inherited it.
- • Track generated-client deserialization errors reported by consumers — they cluster exactly where the spec's nullability and required-ness are wrong.
- • Watch which doc pages and operations agents and humans fail on (retries, malformed calls): failures concentrate where descriptions are emptiest.
- • The governed spec is evolution's substrate: `deprecated: true` flags flow to SDK strikethroughs, spec diffs generate changelogs, and [[removing-fields]] burn-downs key off schema paths.
- • Version the spec with the API: the file for each live version is the authoritative statement of what that version promises, and the diff between versions is the migration guide's skeleton (see [[api-migration]]).
- • As agent traffic grows, invest in descriptions and examples the way you once invested in human docs — they are now the operation-selection interface for a class of consumers that reads nothing else.
- • Spec maintenance is permanent overhead: reviews now cover a YAML artifact plus prose plus code, and the discipline must survive deadline pressure to keep the tooling dividend.
- • The tooling chain (linters, differs, generators, validators) is infrastructure with version skew and quirks of its own — someone owns it or it rots.
- • Machine-checkable shapes create a false-confidence gradient: teams over-trust what is checked and under-write what is not, which is precisely backward from where the risk sits.