API Contracts: OpenAPI, Contract Testing and Docs

Learn OpenAPI anatomy, design-first mocking, request validation, linting and CI diffs, the API test pyramid and Pact consumer-driven contracts.

API contracts and contract testing: common questions

What is the difference between design-first and code-first API development?

Design-first writes and reviews the OpenAPI contract before any code, so mistakes are fixed in the file and clients can build against a mock. Code-first generates the spec from annotated code, which is faster for one team but exposes the design only after it is built.

Why use consumer-driven contract tests if I already have an OpenAPI spec?

A spec diff knows a field was removed but not whether anyone reads it. Pact records exactly which fields each consumer uses and replays those expectations in the provider’s CI, so a breaking rename fails before deploy.

Related chapters

Keep learning

Overview

Which version of the OpenAPI format this file uses, so tools know how to read it. It is not the version of your API. Name and version of the API itself. Bump info.version with every release so docs and changelogs line up (see the evolution chapter). paths → operations Inputs that live outside the body: path, query, header or cookie. Each has a schema, so “eventId must be a string” is checkable. What the client sends, per media type. It points to a schema instead of describing the shape inline. One entry per status code the client should expect, each with its own body schema. A missing 422 here means the docs never tell anyone it can happen. components / schemas The data shapes, written in JSON Schema: types, required fields, limits such as maxItems, and whether unknown fields are allowed. How callers prove who they are. Declared once in components, then required per operation (or for the whole file). $ref (reuse) $ref points to a definition elsewhere, so BookingRequest or the Problem response is written once and reused by every operation that needs it. openapi: 3.1.0 title: Tickets API version: 2.3.0 operationId: createBooking - name: eventId in: path required: true description: Booking created - bearerAuth: [] type: object required: [ticket_ids] additionalProperties: false type: array minItems: 1 maxItems: 6 OpenAPI document for the booking operation Correct: two seats "ticket_ids": ["A1", "A2"] Leave ticket_ids out Send a string, not a list "ticket_ids": "A1" Ask for 7 seats "ticket_ids": ["A1","A2","A3","A4","A5","A6","A7"] "price_inr": 1 ticket_ids is required must be an array of seat ids at most 6 seats per booking, got 7 Passes the schema → forwarded to the booking service\nHTTP/1.1 201 Created\nLocation: /bookings/bk_5Rt9 Lab: break the booking body and watch the validator at the gateway Other mistakes Add an unknown field · price_inr: 1 Forget the closing brace POST /events/123/bookings What comes back Let through The parser fails before the schema is even checked, so this is a 400 and no field errors are listed. Unit tests laptop, seconds Component tests pull request, about a minute OpenAPI diff pull request, seconds Consumer contract verification pull request, before merge End-to-end on staging after deploy to staging, 20+ minutes after release The developer renamed the field and updated the provider’s own tests in the same commit, so they pass. These run the booking service with its database stubbed and check its own expectations, which were updated too. The spec generated from the code lost total_inr from the Booking response. The diff tool flags “response property removed” as breaking and fails the build. The staging journey books two seats and checks for 201 and a booking id. It never reads the total, so it passes. John opens his booking and sees “Total: ₹NaN”. Support tickets arrive before anyone looks at a dashboard. Lab: the provider renames total_inr to amount_inr. Which layer notices? OpenAPI diff in CI Consumer-driven contracts (Pact) fails: caught here not reached Pact would have failed next too, and it would also name the app that breaks.

Contracts: OpenAPI, contract tests and documentation

A contract is one file that says exactly what the API accepts and returns. People read it as docs, and tools read the same file to mock the API, generate clients, reject bad requests and fail the build when a change would break someone.

One source of truth for people and tools

A wiki page about the API Written once, edited by hand, drifts from the code within weeks. Nothing can check it, so nobody trusts it, so people read the code instead. A machine-readable contract openapi.yaml lives next to the code. Docs, mocks, client code and the gateway’s validator all come from it, so if it is wrong, something fails loudly. Write the contract Lint and review style guide Mock it apps start early Build to it server + clients Enforce it validator, CI diff The same file is used at every step, from design to production. the evolution chapter , written down so it can be checked.

Anatomy of an OpenAPI document

OpenAPI (formerly Swagger) describes HTTP APIs in YAML or JSON. Here is the real booking operation from this course. Click each part: Read it top-down like a tree: paths hold operations, operations hold parameters, a requestBody and responses, and those point into components. Since OpenAPI 3.1 the schemas are plain JSON Schema, so the same definitions work with any JSON Schema validator. $ref is what keeps a 200-endpoint file sane: the Problem error body from the payload chapter is defined once and every operation points to it. Fix it in one place and every operation, doc page and client gets the fix.

Design-first or code-first, and mocking before building

Write the handlers, add annotations, generate the spec from the code. Fast for one team, and the spec can’t drift from the code. But the design is whatever the code happened to be, and consumers see it only after it is built. Write the contract, review it with the app team, then build. Mistakes are fixed in a YAML diff instead of a migration. It needs discipline: the build must check that the code still matches the file. boxes on a whiteboard names and actions, no URLs yet OpenAPI + mock server real service Each step is cheaper to throw away than the next. The mobile team builds the booking screen against the mock while the booking service is still being written. If the screen needs a field the contract lacks, they find out in week one, not at integration.

What the contract buys you

Reference docs for readers Every endpoint, field and error rendered as pages with a “try it” box, regenerated on every release. Mock servers for app teams Answers from the examples in the file, so clients are built in parallel with the service. Code generation for both sides Typed clients and server stubs. Saves typing, but generated clients can choke on a new enum value if they are too strict. Validation at the edge at runtime The gateway checks each request against the schema and rejects bad ones before they reach a service. at review A style guide as rules: snake_case fields, every operation has an error response, no verbs in paths. Diff in CI before merge Compare the spec on main with the one in the pull request and fail if a change would break clients. Linting turns the review checklist from the principles chapter into something a robot runs on every pull request. With Spectral it looks like this: Diffing catches the change that looks harmless in code review. A tool such as oasdiff compares two versions of the file and sorts each change into safe or breaking:

Validation at the edge: reject bad requests early

Strict on what you accept additionalProperties: false on request bodies. A typo like ticket_id fails loudly instead of being ignored, and nobody can slip in price_inr or status: "paid" (see the OWASP chapter Tolerant on what you read Clients should ignore unknown fields in responses, so the server can add fields without breaking them. Don’t put additionalProperties: false on response schemas that clients validate against.

Testing an API: which test catches what

how many what is real logic bugs: price rounding, seat rules whole service over HTTP, neighbours stubbed routing, status codes, JSON shape vs the spec service + real Postgres or Kafka in Testcontainers SQL, migrations, serialization, driver quirks per consumer provider vs recorded consumer expectations breaking a field a real client reads a handful everything deployed together wiring and config across services The test pyramid: many cheap tests at the bottom, few expensive ones at the top. A component test starts the booking service and calls POST /events/123/bookings over real HTTP, with the payment service replaced by a stub. An integration test swaps the fake database for a real one: Testcontainers starts Postgres in Docker for the test and throws it away after, so “works with H2, fails in production” stops happening.

Consumer-driven contracts: let the clients write the tests

Consumer test iOS app vs a Pact mock Pact file requests + fields it reads stores pacts per version Provider verify replays them in CI gate before release The iOS team writes a test against a Pact mock: “GET /bookings/9 returns a status string and a total_inr number.” That expectation is saved as a pact file. The booking service’s CI fetches every consumer’s pacts and replays them against the real service. Now try the rename: versioning rules are your safety net.

The same idea for gRPC, GraphQL and events

contract file SDL schema (type Booking…) Events and webhooks AsyncAPI: channels, messages, payload schemas schema registry compatibility rules for topics such as booking.confirmed Every style has the same three pieces: a file that is the truth, a tool that turns it into code and docs, and a check in CI that compares two versions. For how events are delivered, see messaging types (opens in a new tab).

Docs people actually use, and the interview line

Getting started first 10 minutes Get a sandbox key, list events, book two seats, see the booking. One page, copy-paste commands. Goals and use cases “Sell tickets on your site”, “sync Asha’s venue calendar”: each maps to the few endpoints it needs. Built from openapi.yaml, so it is never out of date. Necessary, but nobody learns an API from it. Runnable examples curl + SDKs Real requests and responses for each operation, including the 422 and 409 cases. Error catalogue one page per type Each problem type URL opens a page: what it means, why it happens, how to fix it. what changed Dated entries, breaking changes first, with Sunset dates and migration notes. “The booking API is design-first: an OpenAPI file reviewed with the app team, mocked so the app can start early, and linted against our style guide. The gateway validates requests against it and returns 422 with field pointers. CI diffs the spec to block breaking changes, and the apps’ Pact contracts are verified before every deploy.” The contract also declares bearerAuth on every operation. The next chapter makes that line real: who is calling, what they may do, and how often. GitHub publishes an OpenAPI description of its whole REST API. GitHub’s published contracts Spotify consumers survived 2026 only if they parsed missing fields safely. Breaking-change lab

More courses

  • Jobs, linked to lessons — Browse jobs where every requirement links to the chapter that teaches it. See how ready you are, how long the gap takes to close, and which listings look stale.
  • Match a job post to courses — Paste a job post. Each skill is matched to a track here and put in learning order, foundations first.
  • React from open source with patterns — Patterns extracted from Material UI, Radix, React Aria, React Hook Form, Redux Toolkit, TanStack, and Next.js — not slides about hooks.
  • Database in depth — How a query becomes work. Indexes, isolation, locks, and the plans that explain why a page is slow — not an ORM tour.
  • Build a real API with NestJS — One ticket-booking API, built in order: a plain Node server first, then NestJS modules, validation, a real database, and the features real clients lean on.
  • Microservices — Microservice patterns one diagram at a time: how to split a monolith, how services talk, and how data stays consistent without distributed transactions.
  • Kafka — A beginner-to-senior path through event logs, reliable delivery, production operations, and disaster recovery.
  • AI engineering for software engineers — Eight free reads, in order, for software engineers moving into AI: when to build an agent, how to build one, how to test it, and how the model works underneath.
  • Frontend system design — How a UI is sliced when it is a product, not a page: ownership of state, cache, routes, and what happens when the network lies.
  • TypeScript in depth — The type system as a design tool. Inference, narrowing, generics, and the errors that mean the model is wrong — not a catalog of utility types.
  • CSS in depth — See the box, then the page. Visual chapters from the box model through Flexbox surprises, Grid, selectors, and units, then backgrounds, shapes, and visual effects, each with a playground.
  • Accessibility — Foundations first (WCAG in plain words, semantic HTML, lists, alt text), then hands-on HTML labs, a full pass of WAI-ARIA 1.3, and a final fix-the-page challenge.
  • Docker — What an image is, why layers cache, and how containers find each other on a network.
  • Redis — Fifteen Redis patterns, one picture each: cache, session, lock, counter, rate limiter, queue, and when Redis is the wrong tool.
  • Kubernetes — Kubernetes from zero to operators: why it exists, Pods, Deployments and Services, then configuration, networking, storage, scheduling, scaling and security.