Appearance
9.6.4 — The Contract as an Artifact: OpenAPI, Tooling & the Ideal API
Ask a team where their API is documented and you usually get three answers at once: a wiki page from last year, a Postman collection someone maintains by hand, and "just look at the handler". All three disagree, and the frontend engineer who has to call the thing finds out which one is true by trying it.
That is the actual problem this page solves. Not "we should write docs" — every team agrees with that and it changes nothing. The question is where the contract lives such that it cannot quietly disagree with the running code. A contract kept in prose drifts within a week, because nothing fails when it drifts. A contract kept as a machine-readable file behaves completely differently: it generates the types the frontend compiles against, the mock server they build on, the documentation, and the validation the server runs — so a drift between contract and code becomes a build failure in front of a person rather than a surprise in production. ⚑API documentation (OpenAPI/Swagger). [EQ-977]
1. OpenAPI: the contract, machine-readable
OpenAPI (formerly Swagger — the name survives in tooling) is a YAML/JSON format describing an HTTP API completely: paths, operations, parameters, request/response schemas, auth, errors. A working excerpt, annotated:
yaml
openapi: 3.1.0
info: { title: Orders API, version: 1.4.0 }
paths:
/orders/{orderId}:
get:
operationId: getOrder # (1) stable machine name — SDK method names
parameters:
- name: orderId
in: path
required: true
schema: { type: string, format: uuid }
responses:
"200":
content:
application/json:
schema: { $ref: "#/components/schemas/Order" } # (2) shared, referenced
"404":
$ref: "#/components/responses/Problem" # (3) ONE error shape, reused
components:
schemas:
Order:
type: object
required: [id, status, total] # (4) required vs optional — explicit
properties:
id: { type: string, format: uuid }
status: { type: string, enum: [placed, paid, shipped], description: "Open enum: clients must tolerate unknown values" }
total: { $ref: "#/components/schemas/Money" }Four details in that file matter more than they look.
(1) operationId becomes a method name in someone's code. When a client library is generated from this file, getOrder becomes client.getOrder(id). So name these the way you would name functions in a library you were proud of. Left unnamed, generators invent something from the path and the verb, and your customers end up writing client.getOrdersOrderIdV2() — which is now their code, and renaming it later breaks their build.
(2) and (3) components is where you stop repeating yourself. Define Money once and reference it from every endpoint that returns an amount. Define the error response once and point every failure at it. Skip this and you get five slightly different money shapes across twelve endpoints, one of which uses a float, and no one notices until a client rounds a total wrong. It is the same duplication disease 9.1 describes in code, except the copies live in different files and drift faster.
(4) required and nullability are the fine print that decides whether client code crashes. "Returns an order object" tells a client author nothing about whether total can be absent. A TypeScript client needs to know exactly which fields are optional and which can be null (3.7.2), because that difference is what puts a ?. in their code or a crash in their error tracker. This is precisely the information prose documentation always omits, and it is free here.
Writing the spec first, or generating it from code
Both directions are defensible and each pays for something different.
Spec first means you write the YAML, get it reviewed, and then implement it. The contract genuinely gets designed as a contract, by people looking at it as a whole, before any handler exists to make its shape feel inevitable. It also unblocks the frontend immediately, because a mock server can serve the spec on the day it merges. The cost is that you now maintain a document that can silently disagree with your implementation, unless you check.
Code first means the spec is generated from your handlers and their schemas. Nothing can drift, because there is only one source. The cost is the reverse: your "contract" ends up being whatever the code happened to do, and the design review — if it happens at all — happens after the code is written and nobody wants to change it.
In a TypeScript codebase there is a way to get both. Define your schemas once using Zod (3.7.7 covers it as boundary validation), then derive three things from that one definition: the runtime validation your server actually runs, the TypeScript types your code uses, and the OpenAPI document you publish. Now the published contract cannot disagree with the validation, because both were built from the same object, and you can still review the generated spec as a design artifact before release.
Whichever way you go, one rule does not bend: the spec has to be checked against the running service automatically. Run your test suite through validation middleware that fails when a response does not match the schema it claims. Without that check, a spec is just documentation, and documentation always ends up lying — not because anyone lied, but because someone shipped a fix on a Friday and the YAML was in a different folder.
2. The toolchain: what the artifact buys
The reason to keep a spec is not tidiness. It is that one file produces six things you would otherwise build and maintain separately.
Types the frontend imports. A generator turns the schemas into TypeScript types, so the client's own compiler checks every call against the contract. That is worth pausing on: the network boundary — the place where types normally stop and everything becomes any — now has a compile-time check across it (3.7.1). When the backend renames a field, the frontend's build turns red the same day, instead of a screen going blank next month.
Documentation that cannot rot. Tools like Swagger UI or Redoc render the spec into a browsable page with a working "try it" console. Because it is generated on every merge, it is always describing the current API. There is no version of this that goes stale, because there is nothing separate to update.
A mock server, free. Point a tool such as Prism at the spec and it serves fake responses that match the schemas. The frontend can build the entire screen against that mock the day the spec merges, weeks before a single handler exists. This is the concrete payoff of designing the spec first — two teams working in parallel instead of one waiting. ⚑API mocking. [EQ-979]
A Postman collection for exploring and for support. Generate the collection from the spec, and use it for what it is good at: a shared workspace with authentication pre-wired, one environment per stage, and a saved request that reproduces a customer's bug. What it must never become is the source of truth. A hand-curated collection drifts exactly like a wiki page, and then two artifacts describe your API. Generate it, do not groom it. ⚑Postman collections. [EQ-978]
The validation your server actually runs. The same schemas that describe the API also check incoming requests at runtime; 9.9 shows this wired into Express. When the published contract and the enforced validation are the same object, "the docs said it was optional but the server rejects it" stops being possible.
A gate that catches breaking changes before they ship. Tools that diff two versions of a spec can classify each change as safe or breaking, using exactly the catalog from 9.6.3. Wire that into CI and a pull request that removes a field fails automatically. Every team has a rule that you must not break clients; this is the version of that rule which works at 6pm on a Friday.
3. The ideal API: the checklist
DrillCuriosity #57 (verbatim): Things to consider while api designing. What an ideal api looks like? As a frontend developer who should I ask backend team to design
The complete demand list, folder-referenced — bring this to the design meeting. Contract & docs: an OpenAPI spec (not a wiki page), with every schema's required/nullable explicit, every error enumerated, and a mock server URL so the frontend starts today (section 1–2).
Predictable resources: guessable plural-noun URLs, honest verbs (GET never mutates), one casing and one date format (ISO 8601 UTC) everywhere (9.6.1). Status codes as types: 201-vs-200, 401-vs-403, 422-with-field-errors — never 200-with-error-body (9.6.1 section 3).
One error envelope: machine codes stable forever, per-field validation errors (the form must paint the right input red), traceId in every error for support (9.6.1 section 4). Collections that scale: cursor pagination with pageInfo, documented filter/sort grammar (unknown params rejected loudly), thin default projections (9.6.2).
Write-safety: idempotency keys on non-idempotent writes (the retry-on-flaky-mobile story), ETags + If-Match where edits race, and a 409/412 contract the UI can build conflict screens on (9.6.3). Evolution promises in writing: additive-only within a version, enums documented open, deprecations announced with Sunset headers (9.6.3 section 3).
How pleasant it is for the frontend:
- Responses are shaped for screens, not for database tables. If one screen needs 4 calls and has to join the results itself, ask for an endpoint built for that screen (9.6.1).
- By default, responses do not send more than the screen needs.
- Expensive fields are only sent when asked for.
- "Nothing found" always looks the same, either
[]or404, decided once.
Operations: documented rate limits with 429 + Retry-After, CORS configured for your origins ([6.10]), auth flows specified end to end (which token, where, refresh how — Part 8.4), and sandbox credentials that work. That's the ideal API: boring, guessable, written down, and safe to retry — and every line is a chapter reference, which is the real answer to "things to consider while designing."
4. The expert lens
The spec is an interface, so everything 9.1 says about interfaces applies to it. Keep related things together and defined once — one Money schema, not five. Watch what clients bind to, because operationIds and error codes end up hardcoded in other people's software and are therefore permanent from the moment you publish them. And accept that the permanence law applies here too, which is what the CI diff gate is for.
There is a single question that reveals how a team really regards its API: does changing a schema get the same review as changing a database migration? Teams that treat the spec as paperwork produce drift, because paperwork has no consequences. Teams that treat it as the most-used interface their product has get everything in section 2 for free.
Mocks move the contract to the beginning of the schedule, and that is worth more than it sounds. The usual sequence is that the backend builds for three weeks, the frontend waits or guesses, and integration week is where everyone discovers they understood the same sentence differently. With a spec and a mock, the sequence becomes: review the contract, start the mock, both sides build against the same artifact, and integration is mostly a formality.
The obvious gain is parallel work. The deeper gain is where the design mistakes get found. In a spec review, a wrong field name costs a comment. In integration week, the same mistake costs code on both sides plus the argument about who changes. Moving errors earlier is the entire argument for designing the contract before the handlers.
The checklist in section 3 is for negotiating, not for grading. Real backend teams have legacy shapes, real deadlines, and reasons you have not heard yet. Walking in with a list of best practices and no costs attached gets you a polite ticket in a backlog.
What works is naming the price of each missing item. "No cursor pagination" is not a style disagreement; it means the mobile app will show duplicate rows to users while they scroll (9.6.2). "No field-level validation errors" means every form in the product can only show one generic error banner, so users abandon signup. "No idempotency keys" means support tickets about double charges, and someone has to refund them by hand.
That translation — from a principle into a consequence somebody already cares about — is the skill this folder was really teaching.
What the interviewer will push on
This material comes up less as a whiteboard question and more as a way of finding out whether you have worked with other teams.
"How do you keep documentation from going out of date?" They are checking whether you reach for process or for mechanism. Anyone can say "we should keep docs updated"; the answer that lands is that the documentation must be generated from something that fails a build when it is wrong. Say that the spec, the runtime validation, and the client types come from a single definition, and that responses are validated against it in your test run. The weak answer is any policy that depends on a person remembering.
"Spec first or code first?" They want your reasoning, not your team's convention. Give both costs honestly — spec first can drift from the implementation, code first turns whatever you built into the contract by default — and then say what you would do and why. Mentioning that a single schema definition can produce all three artifacts is the answer of someone who has actually tried to keep them in sync.
"The frontend is blocked waiting on your endpoint. What do you do?" They are looking for the mock. Merge the schema, serve a mock from it, let the frontend build against it today. The follow-up is the interesting part: how do you make sure the real endpoint matches the mock they built against? Answer: because the mock came from the same file the server validates against, and CI fails if they diverge.
"How would you stop someone accidentally shipping a breaking change?" Name the automated gate — a diff of the spec against the previous release, classifying removals, renames, type changes, and newly required fields as breaking, and failing the pull request. Then add the human half: the breaking-change list is written down, and a schema change gets reviewed like a migration. Relying only on review is the answer of a team that has not been burned yet.
Volunteer this one, because nobody asks: say that the operationId values and the error code strings are permanent public API the moment they ship, because they become method names and branch targets in other people's code. Almost everyone treats them as internal labels and renames them freely for a year, then discovers they cannot. Naming them carefully on day one costs nothing and is impossible to fix later.
Next: Part 9 moves to the interview arena. Chapter 9.7 opens the LLD machines folder, taking classic design problems from requirements, to entities, to interfaces, to working TypeScript.
Recall
- OpenAPI = the machine-readable contract: paths/operations (
operationId= SDK names — name them well),componentsfor DRY schemas + one error response, explicitrequired/nullability, open enums documented. Spec-first parallelizes via mocks; code-first can't drift; the TS synthesis: Zod as single source → runtime validation + OpenAPI + types derived. Unverified specs are documentation; verify in CI. - The multiplication: one artifact → types/SDKs (drift = frontend compile error) · docs (regenerated, can't rot) · mock servers (frontend starts on merge day) · Postman (a regenerated view, never the truth) · runtime validation (same schemas) · breaking-change diff gates in CI.
- The ideal-API checklist (Curiosity #57): spec+mock, guessable resources, honest codes, one error envelope (stable
codes, field errors,traceId), cursor pagination + declared grammar, idempotency keys + ETags, additive-evolution promises in writing, screen-shaped responses, documented rate limits/CORS/auth. Boring, guessable, written down, safe to retry. - Lens: the spec is the most-consumed interface — review it like a migration; mocks shift design errors from integration week to spec review; ask with costs attached, not "best practice says."
Self-test: What do operationId and components each buy? State both spec/code-first costs and the Zod synthesis. Name six artifacts one spec generates. Recite eight checklist categories from memory. Why does "cost attached" beat "best practice" in API negotiations?
Quiz Bank
FoundationalWhat is OpenAPI, and which parts of the document carry the most contract weight?
OpenAPI is a standard, written in YAML or JSON and formerly called Swagger, that describes an HTTP API in a form programs can read. It has three main parts: paths (each route's operations, parameters and responses), components (shared schemas, responses and auth), and info (name and version).
The parts that matter most:
operationIds. They become the method names in generated SDKs, so once published they can never change (3.6.1).requiredlists and nullability. They decide which fields are optional in client types (3.7.2). Written docs almost always leave these out.- Shared
components. OneMoneyschema and one Problem Details error, used everywhere. Five copies of a schema will drift apart, which is the duplicated-knowledge problem of 9.1, in YAML. - Marking enums as open. Writing "clients must handle unknown values" into the schema is what lets you add enum values safely later (9.6.3).
The bigger point: none of this matters unless CI checks that the spec matches the real API. A spec that nothing checks slowly turns into fiction.
FoundationalSpec-first vs code-first vs schema-derived — costs, benefits, and the TypeScript-shop synthesis.
Spec-first (author YAML, implement after): the contract gets designed — reviewable before code exists, and a mock server from the spec lets frontend and backend build in parallel from day one; costs: YAML authorship, and spec/implementation divergence unless runtime-verified.
Code-first (generate spec from handlers/annotations): divergence structurally impossible — the code is the source; costs: the contract is whatever the code grew into (design review arrives after implementation), and handler annotations are a weaker design medium than a reviewed document.
Putting it together for TypeScript teams: make Zod schemas the single source of truth. The same schema object gives you three things: runtime validation (3.7.7), static types through z.infer, and the OpenAPI document through a zod-openapi generator. So the contract, the checks and the types are guaranteed to match. And the generated spec still supports review, mock servers, SDKs and blocking breaking changes. Direction matters less than the invariant:
one source, everything derived, verified in CI — the same derive-don't-duplicate law as 3.7.5, applied to the network boundary.
AppliedYour frontend team starts a feature Monday; the backend's endpoints land in three weeks. Lay out the OpenAPI-powered workflow that removes the dependency.
Week 0, now: both teams review the spec together. The endpoint's schema is agreed as YAML, or as a draft generated from Zod. That covers its shapes, which fields are required, the error codes and the pagination format. The frontend's items from the section 3 checklist are raised here, as comments on the spec. Words are cheap now. The same problems found in integration week would not be.
Merge the spec, and CI publishes three things:
- A Prism mock server at a fixed URL, returning examples that match the schema. The frontend uses it as
API_BASE_URLfor three weeks. - Generated types from
openapi-typescript, which the frontend imports. Its components are typed against the real contract from day one. - Fresh docs, and a Postman collection for exploring the API.
Weeks 1–3 in parallel: frontend builds against the mock (MSW mirrors the same schemas in component tests); backend implements with the same schemas as validation middleware — both sides bound to one artifact, neither waiting. Integration day: swap the base URL; because both sides were CI-verified against the identical contract (backend: request/response validation in its test suite; frontend: compile-time types + MSW), integration is a smoke test, not an archaeology week. Drift protection forever after: spec diffs gate the backend's CI (breaking-change classifier), and the frontend's type-generation step turns any break that slips through into a compile error rather than a runtime surprise.
InterviewWhat belongs in an API design review — the gate checklist a reviewer runs before an endpoint ships?
Run the folder as a gate: Modeling — plural-noun resource or honestly-reified action (9.6.1 section 2); nesting only for true ownership; IDs opaque; casing/dates house-standard. Verbs & codes — GET safe, PUT/DELETE idempotent, PATCH semantics stated; 201/202/204 used honestly; 401/403 and 400/422 lines consistent; no 200-with-error.
Errors — house envelope, machine codes (added to the stable registry), field-level validation errors, traceId. Collections — cursor pagination default (offset only with cap + justification); filter/sort whitelist with the index plan attached (9.6.2 section 6 — grammar and index are one PR); projection defaults thin.
Write-safety — idempotency-key requirement for non-idempotent writes; ETag/If-Match for contested mutables; the crash-window transaction story told (9.6.3 section 1). Evolution — additive within version; enums declared open; the spec diff classified non-breaking, or the versioning conversation had.
Spec & tooling — OpenAPI updated (schemas via shared components), examples valid, mock regenerated, generated types compiling in consumer CI. Ops — rate limits + 429 documented; auth flow specified; CORS origins listed. The review's output is 9.3.1-style: findings with named costs, not taste — and the checklist's existence is what turns API quality from heroics into process (9.6.1's staff-answer org lesson, operationalized).
StaffYour org has 30 services, each with hand-written docs of varying rot, three SDK styles, and integration bugs dominated by 'the API didn't match the docs.' Design the contract-infrastructure program: what you mandate, what you build centrally, and how you measure success.
Diagnose first. When "the API does not match the docs" keeps happening, it means the contract exists only as written prose. Nothing can check prose, so it is bound to become wrong (9.1).
Set rules about outcomes, not tools (the lesson from 9.4.24):
- Every service publishes an OpenAPI spec that is either generated from the code or checked against it in CI. Hand-written YAML is allowed only if validation middleware enforces it at runtime. For TypeScript services, generating it from Zod schemas is the recommended path.
- Spec changes go through the same review as database migrations. CI runs a tool that flags breaking changes, using the list of safe additions from 9.6.3. That enforces "only add, never break" across the whole company.
- The company's standard error body and pagination style become rules a linter can check in the spec, using a Spectral-style linter with custom rules. That is the section 3 checklist, turned into code.
Build these once, centrally. This is where a platform team pays off most:
- A spec registry that holds every service's contract, versioned and searchable. The company's API catalogue comes for free.
- One SDK generation pipeline per language, built from the
operationIds, with a naming check. Three different SDK styles become one. - A hosted mock server for every merged spec, so any team can build against any other team's API right away.
Adoption mechanics: platform team migrates the 3 highest-traffic services as the exemplar (working code beats memos), then a paved-road policy — new services get the pipeline by template; existing services adopt on their next breaking incident (they will).
Measure:
- How many "the API does not match the docs" tickets come in. It should fall to almost zero, because the docs now are the checked spec.
- How long a new team takes to start using an API. The spec and the mock should cut it sharply.
- How many breaking changes are caught in CI, against how many reach production.
- What share of services follow the standard path.
Refuse: a big-bang rewrite of 30 specs by a tiger team (they'd rot again — the pipeline is the deliverable, not the YAML), and mandating one web framework to get code-first "for free" (the invariant is verified contracts; the spelling is per-team — 9.4.24's survival rule again). Success sentence: docs that cannot lie, SDKs nobody hand-writes, and integration bugs demoted from category to anecdote.
Flashcards
FlashOpenAPI weight-bearers
operationId (= SDK names, permanent) · required/nullability · shared components (one Money, one error) · open-enum annotations. Unverified spec = fiction.
FlashOne spec generates
Types/SDKs (drift = compile error) · docs (can't rot) · mock servers (day-one frontend) · Postman views · runtime validation · CI breaking-change diffs.
FlashThe TS synthesis
Zod = single source → runtime validation + z.infer types + generated OpenAPI. One source, everything derived, CI-verified.
FlashMock-powered parallel workflow
Spec review → merge → Prism mock + generated types → both sides build 3 weeks in parallel → integration = smoke test.
FlashIdeal API in one line
Boring, guessable, written down, safe to retry — spec+mock, honest codes, one envelope, cursors, idempotency, additive evolution, screen-shaped.
FlashNegotiating APIs
Ask with the cost attached ("no field errors = generic toast on every form"), not "best practice says." Priced consequences move backlogs.
Scenario Drill
DrillYou're the frontend lead consuming a new internal /campaigns API; the backend team shares their draft OpenAPI spec for review tomorrow. Prepare the review: the ten things you check in the YAML, the three you push back on hardest if missing, and how you turn the meeting into a lasting workflow rather than a one-off win.
The ten checks, spec-section by section. (1) components/schemas: is there one shared error schema (Problem-Details-shaped: code, errors[], traceId) referenced by every non-2xx response — or per-endpoint improvised errors? (2) required arrays: is optionality explicit on every schema, and do nullable fields say so (your generated types are only as honest as this — 3.7.2)? (3) Enums: annotated open ("tolerate unknown values") or silently closed — the additive-evolution hinge (9.6.3)? (4) Collections: cursor envelope (pageInfo.nextCursor) or offset params — and if offset, is there a documented depth story (9.6.2)? (5) The filter/sort grammar: declared and whitelisted, with unknown-param behavior specified (422, not ignore)?
(6) List projections: thin defaults with heavy fields opt-in — or 40-field items on the browse endpoint? (7) Writes: Idempotency-Key documented on campaign-create (marketing will double-click), ETags on campaign-edit (two marketers will race — the lost-update UI needs the 412 contract)? (8) Status codes: 201+Location on create, 202 semantics if activation is async, 401/403 and 400/422 lines consistent? (9) operationIds: SDK-worthy names (getCampaign, activateCampaign)? (10) Dates/casing/IDs: ISO-8601-UTC, one casing, opaque IDs — the boring trifecta.
The three hardest pushbacks, each with its cost stated (as section 4 teaches):
- Errors per field. Without them, every form failure is one vague pop-up, and our error messages are useless from day one.
- Cursor pagination. The campaign list is the product's home screen. With offsets, rows shift while marketers scroll, they see duplicates, and they file "the data is wrong" tickets against us.
- The mock server. Without it, our three sprints wait in line behind yours. With it, we build at the same time, and joining up at the end is a quick check.
Turn it into a routine. End the meeting by proposing a process, not just the fixes:
- When a spec is merged, CI publishes a Prism mock and
openapi-typescripttypes that your app imports. If they break the contract, you get a compile error. Neither team has that early warning today. - The ten checks become the shared review for every future endpoint, saved as
API_REVIEW.md. - Both teams use comments on the spec as the place to discuss design.
The lesson of this whole folder: the cheap place to fix an API is in its YAML. The expensive place is in two codebases three weeks later.