API Documentation Is Not an Afterthought. It Is Part of the Design.
by Eric Hanson, Backend Developer at Clean Systems Consulting
When documentation reveals design problems
The clearest sign that an API design needs work is when its documentation requires extensive explanation. If a single endpoint needs three paragraphs of caveats — "but only if the account type is X," "unless the flag was set during creation," "note that this field means something different for batch requests" — that is a design problem surfaced through documentation.
Writing documentation before you build is design work. It forces you to explain the API from the outside in, as a developer who has no access to your implementation. Every awkward sentence, every edge case you find hard to explain, every conditional behavior you have to caveat — these are signals about the design itself.
The practice is not new: it parallels test-driven development. Write the spec first. If the spec is hard to write, the implementation will be hard to use.
OpenAPI as the contract
OpenAPI (formerly Swagger) is the standard format for describing REST APIs. An OpenAPI spec is machine-readable, can generate client SDKs in dozens of languages, powers interactive documentation tools (Swagger UI, Redoc, Scalar), and can be used for contract testing.
A minimal but complete OpenAPI spec for a single endpoint:
openapi: 3.1.0
info:
title: Orders API
version: 1.0.0
paths:
/orders/{id}:
get:
summary: Retrieve an order by ID
parameters:
- name: id
in: path
required: true
schema:
type: string
pattern: '^[0-9A-Z]{26}$'
description: ULID of the order
responses:
'200':
description: Order found
content:
application/json:
schema:
$ref: '#/components/schemas/Order'
'404':
description: Order not found
content:
application/json:
schema:
$ref: '#/components/schemas/ApiError'
The pattern ^[0-9A-Z]{26}$ in the parameter schema tells the developer what a valid ID looks like without them having to try invalid values and debug 404s. The 404 response schema tells them exactly what comes back when an order is not found. Both of these are documentation decisions that prevent support tickets.
What the spec cannot do alone
OpenAPI describes structure. It does not describe intent.
A field named status with type string is structurally documented. But a developer needs to know: what values are possible? Are they stable or can they expand? Does the transition between states follow a defined state machine? What operations are available at each status?
This context belongs in the description field of your schema — not in a separate wiki page, not in a comment in someone's Slack message:
components:
schemas:
Order:
properties:
status:
type: string
enum: [pending, confirmed, shipped, delivered, cancelled]
description: |
Current state of the order.
- pending: payment received, not yet fulfilled
- confirmed: warehouse has acknowledged the order
- shipped: tracking number issued
- delivered: confirmed by carrier
- cancelled: order voided; see cancellation_reason
New status values may be added in future API versions.
Clients must handle unknown status values gracefully.
The last line — "clients must handle unknown status values gracefully" — is a versioning policy embedded in the documentation. A developer reading this knows to build a default case into their status handling logic.
Examples are not optional
Abstract descriptions and schema definitions tell developers what is allowed. Examples tell developers what is typical. Both are necessary.
Every request body and response schema should have at least one example. For complex endpoints, include multiple examples covering the main cases:
content:
application/json:
examples:
physical_product:
summary: Order for a physical product
value:
id: "01HZQK7P3WVXBN4Y9MRDTJC8E6"
status: "shipped"
tracking_number: "1Z999AA10123456784"
digital_product:
summary: Order for a digital download
value:
id: "01HZQKBRW4X9L3F7G2ESHYD0M8"
status: "delivered"
download_url: "https://cdn.example.com/files/..."
A developer reading these examples understands that physical orders have tracking numbers and digital orders have download URLs, without you having to explicitly document the conditional logic.
The changelog is part of the documentation
Every API change — breaking or non-breaking — should be in a changelog. Not just "bug fixes and improvements." The changelog is the document a developer reads when their integration breaks:
## 2026-04-19
### Added
- `GET /orders` now supports `status` filter parameter (e.g., `?status=shipped`)
- Orders now include `estimated_delivery` field (nullable, present when order is shipped)
### Changed
- `POST /orders` now validates that `shipping.postal_code` matches the format
for the specified `shipping.country`. Previously accepted any string.
(This is a behavioral change; the request schema is unchanged.)
### Deprecated
- `shipping_address` string field is deprecated in favor of `shipping` object.
Will be removed in v3. See migration guide.
Behavioral changes — like the postal code validation tightening — are especially important to document. They are not schema changes, so schema diffing tools will not catch them. The changelog is the only place they live.
Keep the spec close to the code
Documentation that lives in a separate repository or wiki falls out of sync. The OpenAPI spec should live in the same repository as the API code and be validated in CI:
npx @redocly/cli lint openapi.yaml
Better: generate the spec from code annotations and export it as an artifact on every build. The spec is always current because it is derived from the running code. Tools like springdoc-openapi (Java/Spring), drf-spectacular (Django), and FastAPI's built-in generation support this pattern.