Why hand-written reference drifts by construction
A hand-written reference page is a second copy of the interface, kept in a different file, edited by a different person, at a different time, under no pressure from a compiler or a test. Nothing in the system connects the two. When a field becomes optional, the code changes and the page does not, and the failure is invisible - the docs still build, the site still renders, and the page is simply wrong until a support ticket surfaces it. Drift is therefore not a symptom of a lazy team. It is the expected behaviour of two independent sources of truth, and the only reliable fix is to stop having two.
That is what generating a reference from a machine-readable description of the API buys you. Paths, methods, parameter names and types, request and response schemas, required-versus-optional, enum members, status codes: all of it becomes a build artefact derived from one source. If the derivation runs in CI and the site is rebuilt on merge, the mechanical half of the reference cannot lag the code by more than one deploy.
What a spec cannot carry
The parts of reference documentation that people actually get stuck on are the parts a schema has no field for. A schema can say status is one of three strings; it cannot say which transitions are legal or which of them is terminal. It can list a 409; it cannot say that a 409 here means a concurrent write and is safe to retry, whereas the 409 on the adjacent endpoint means a duplicate business key and retrying will never succeed. It cannot express that you must create a reservation before you capture it, that a token is scoped per tenant, or that the endpoint is cheap to call once and ruinous to call in a loop.
OpenAPI gives you description fields to put that prose in, which is the right place for it precisely because it travels with the definition rather than sitting in a separate page that can be deleted independently.
paths:
/v1/payments/{id}/capture:
post:
summary: Capture an authorised payment
description: >
Call only after /v1/payments has returned status=authorised.
Capturing twice with the same Idempotency-Key returns the original
result; without the header a second call is a second capture.
responses:
"409":
description: >
Payment is not in the authorised state. Not retryable; refetch
the payment and inspect status before deciding what to do.
Everything above the 409 could be generated. The two description blocks could not, and they are the reason a reader stops filing tickets.
Where the generation becomes the staleness risk
This is the half candidates skip. A generated reference feels authoritative, which makes its failures harder to notice than a stale hand-written page.
If the spec is a hand-maintained file sitting beside the code, you have not removed the second source of truth, you have renamed it. The spec drifts exactly as the page did, and now it drifts behind a facade of machine generation. If the spec is derived from annotations in the code, the annotations can be wrong in ways the compiler tolerates - a documented type that no longer matches the serialiser, an example left over from a rename. And a generated page is complete by construction, so its silence is easy to miss: an endpoint that has been soft-deprecated for two quarters still renders as a first-class entry, and an internal route that gained a public annotation by copy-paste appears on your public site.
The defences are all mechanical. Require the spec to change in the same pull request as the handler, so a reviewer sees the interface diff next to the implementation diff. Validate real request and response payloads against the spec in your integration tests, which catches the annotation that no longer reflects the serialiser. Lint the spec itself: fail the build when an operation has no description, when a documented example does not validate against its own schema, or when a response is declared with no explanation. And review the rendered diff, not only the source diff, because that is where an unintended endpoint becomes visible.
What the pipeline cannot rescue
None of this touches the prose that explains why the API is shaped as it is: the guide that walks a first integration end to end, the page on retry and idempotency policy, the migration note for a breaking version. Those have no generator and no test, so they need the one thing generation removes the need for - a named owner and a visible review date. Treating them as a small, explicitly owned set is honest. Pretending the pipeline covers them is how a technically current reference ends up useless.
Generation guarantees your reference matches the spec. Nothing except a test against real traffic guarantees the spec matches the service.
© 2026 Preptima. Originally published at preptima.com (opens in a new tab).