Skip to content

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).

Frontend-shaped how pleasant it is to use: responses shaped for screens, not tables (the 9.6.1 re-shard test's consumer side — if a page needs 4 calls and client-side joins for one screen, ask for a purpose-shaped endpoint or aggregate); no over-fetching by default; expensive fields opt-in; consistent empty-state semantics ([] vs 404 — 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), components for DRY schemas + one error response, explicit required/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?

A YAML/JSON standard (née Swagger) describing an HTTP API machine-readably: paths (operations, parameters, responses per route), components (shared schemas, responses, auth), info/versioning. The weight-bearing parts: operationIds — they become generated SDK method names, so they're permanent API surface (3.6.1's law applies to them); required arrays and nullability — the optionality facts that decide client types (3.7.2) and that prose docs perennially omit; shared components — one Money, one Problem-Details error referenced everywhere (five drifting copies of a schema is 9.1's duplicated knowledge, in YAML); enum openness annotations — "clients must tolerate unknown values" written into the schema is what keeps enum growth additive (9.6.3). And the meta-answer: none of it matters unless CI verifies spec-vs-reality (validation middleware in tests, contract tests, diff gates) — an unenforced spec converges on 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.

The synthesis for TypeScript shops: Zod schemas as the single source of truth — the same schema object yields runtime validation (3.7.7), static types (z.infer), and the OpenAPI document (zod-openapi generators): contract, checks, and types provably identical, while the generated spec still supports review, mocks, SDKs, and diff-gating. 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): joint spec review — the endpoint's schema negotiated as YAML (or Zod-derived draft): shapes, required-ness, error codes, pagination envelope; the frontend's [section 3 checklist] items land here, as spec comments (cheap words, not integration-week discoveries).

Merge the spec → CI publishes: (1) a Prism mock at a stable URL serving schema-valid examples (frontend's API_BASE_URL for three weeks); (2) generated types (openapi-typescript) the frontend imports — components typed against the real contract from day one; (3) regenerated docs + Postman collection for exploration.

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: the bug class "API ≠ docs" means contracts exist only as prose — unverifiable, so inevitably false (9.1: hidden coupling to fictional interfaces). Mandates (invariants, not tools — the 9.4.24 org-standard lesson): (1) every service publishes an OpenAPI spec derived or CI-verified — hand-written YAML allowed only with validation middleware proving it in tests; the Zod-derived path recommended for TS services; (2) spec changes ride the same review gate as schema migrations, with a breaking-change classifier in CI (additive catalog from 9.6.3, automated) — org-wide permanence enforcement; (3) the house error envelope and pagination conventions become spec-lintable rules (Spectral-style linters with a custom ruleset — the [section 3 checklist] as code).

Build centrally (the platform team's leverage): a spec registry (every service's contract, versioned, searchable — the org's API catalog falls out free); one SDK generation pipeline (three styles → one, per language, from operationIds — naming lint included); hosted mocks per merged spec (any team prototypes against any service's contract immediately); and consumer-driven contract-test scaffolding ([14.3]) for the critical seams where spec-validation isn't enough.

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: the founding bug class — "API ≠ docs" ticket rate (should approach zero, since the docs are the verified spec); integration lead time for new consumers (spec+mock should collapse it); breaking-change incidents caught in CI vs production; % services on the paved road.

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, costs attached (section 4's discipline): field-level validation errors — without them, every form failure is one generic toast and our error UX is dead on arrival; cursor pagination — the campaign list is the product's home screen; offset drift means marketers see duplicate rows mid-scroll and file "data is wrong" tickets against us; the mock server — without it our three sprints serialize behind yours; with it we ship in parallel and integration is a smoke test. Making it a workflow: close the meeting by proposing the pipeline, not just the fixes — spec merged ⇒ CI publishes Prism mock + openapi-typescript types your app imports (their breaking change becomes your compile error, the drift alarm neither team has today); the ten checks become the shared review gate for every future endpoint (this drill's list, committed as API_REVIEW.md); and the two teams adopt the spec-comment channel as the design venue — because the meta-lesson of this folder is that the cheap place to fix an API is in its YAML, and the expensive place is in two codebases three weeks later.