Documenting APIs with OpenAPI
A specification that is generated from code drifts less; one written by hand stays readable. The useful answer is usually both, with a lint gate.
A specification worth having
openapi: 3.1.0
info:
title: Orders API
version: 2.3.0
servers:
- url: https://api.example.com/v2
paths:
/orders/{id}:
get:
operationId: getOrder
summary: Fetch one order
parameters:
- name: id
in: path
required: true
schema: { type: string, pattern: '^[0-9]{4,10}$' }
responses:
'200':
description: The order
content:
application/json:
schema: { $ref: '#/components/schemas/Order' }
examples:
pending:
summary: A newly placed order
value: { id: "1042", status: "pending", total: { amount: 2500, currency: "GBP" } }
'404':
$ref: '#/components/responses/Problem'
components:
responses:
Problem:
description: RFC 9457 problem details
content:
application/problem+json:
schema: { $ref: '#/components/schemas/Problem' }
schemas:
Order:
type: object
required: [id, status, total]
properties:
id: { type: string }
status: { type: string, enum: [pending, fulfilled, cancelled] }
total:
type: object
required: [amount, currency]
properties:
amount: { type: integer, description: Minor units, e.g. pence }
currency: { type: string, pattern: '^[A-Z]{3}$' }
Problem:
type: object
required: [type, title, status]
properties:
type: { type: string, format: uri }
title: { type: string }
status: { type: integer }
detail: { type: string }
instance: { type: string, format: uri }- Every operation needs a stable
operationId; it is what generated clients name their methods after. - Give at least one response example per status code. Examples remove more support tickets than prose.
- Model errors once in
componentsand reference them, so every endpoint documents the same failure shape. - Put units in the description, not in the field name:
amountin minor units is a documented decision,amount_penceis a naming compromise that leaks into every language.
Keeping the specification true
# 1. Lint the document in CI
npx @redocly/cli lint openapi.yaml --extends recommended
# 2. Check it is a breaking-change-free evolution of the published version
npx oasdiff breaking --base published.yaml --revision openapi.yaml
# 3. Generate the docs and the clients from the same file
npx @redocly/cli build-docs openapi.yaml -o docs/api.html
npx openapi-typescript openapi.yaml -o src/types/api.d.ts
# 4. When the spec is generated from code, fail the build if it changed
# without a version bump
git diff --exit-code openapi.yaml || echo "spec changed: review and version it"| Approach | Strength | Weakness |
|---|---|---|
| Design-first | The contract is reviewed before implementation | The implementation can drift from the spec |
| Code-first generation | Cannot drift from the handler signatures | Comments and descriptions rarely get written |
| Hybrid | Generated structure, hand-written description and examples | Needs a discipline to keep the overrides in the source |
| Contract tests | Proves the implementation matches the spec | Extra test surface to maintain |
💡
A specification that is not enforced is worse than no specification, because clients trust it. Make the CI job that checks the spec against live responses a required status check, so drift fails the build instead of reaching a consumer.
FAQ
OpenAPI or GraphQL SDL?
They do different jobs. OpenAPI describes HTTP endpoints and their status codes; GraphQL describes a type system and a single endpoint. A service can legitimately have both surfaces.
How do I document webhooks in OpenAPI?
Use the top-level webhooks object in OpenAPI 3.1, which describes the requests your service sends to a consumer's endpoint, including the payload schema and signature header.
Related
Testing and contract testing REST APIs Versioning and error shapes
Last refreshed 2026-09-18.