← Back to The book shelf
#014 v11

Generated specs drift away from review

/4 min read

"Generate the API docs from the code — then they can never lie." It's the most settled advice in the room, and in NestJS it's a decorator import away. On a small weather API I refused it on purpose, and the reason wasn't taste: generation doesn't remove drift between code and contract. It decides which of the two drifts unreviewed.

The file everyone had read

The project's brief defined the endpoint contract "in openapi.yaml" — a file that, at first, didn't exist in the repo. I proposed one reverse-engineered from the upstream's live response; the client sent the canonical version; they matched, and the build proceeded against that file. (The fuller story of that exchange is its own post.) What matters here is the sequence: by the time any application code existed, the yaml had been read and accepted by everyone involved.

That reading is the whole source of the file's power. The spec could bind both sides because both sides had reviewed it. A contract's authority comes from the review, not from the file format — and anything that quietly changes a reviewed artifact is spending authority it doesn't have.

A decorator away from a second spec

NestJS convention says: sprinkle @Api* decorators over the controllers and DTOs and let the framework emit the spec. The project's decision record refuses in one sentence — scattering decorators "would duplicate that contract across the code and create a second spec that can drift from the file everyone reviews."

A generated spec drifts silently because regeneration happens downstream of review: at build time, from whatever the code now says. Change a DTO constraint and the published contract changes with it. No reviewer signed the new promise, nothing forces anyone to notice, and the artifact with all the authority has become the one nobody is watching.

Aiming the drift

The shipped setup serves the committed openapi.yaml directly — static mode, no generation — with a CI step that validates the file. The cost is written into the same decision record: change a validation rule in a DTO and you must mirror it in the yaml by hand.

That cost is the mechanism, not an unfortunate side effect. Code and contract are edited by different processes, so they will disagree eventually; the only real decision is which one chases the other. Keep the spec hand-maintained and the code chases the contract — a mismatch surfaces as a failing check or a wrong response that someone can condemn by pointing at the approved file. Generate the spec and the contract chases the code, silently. Same drift, opposite direction, and only one direction is catchable. I wrote in Rule drift that every rule needs one canonical home; the sharper version I'd write now is that when the candidates for home are the code and the thing a stakeholder approved, the approved thing wins.

The Monday check: pick one API you own and ask which artifact a stakeholder actually signed off on. Then ask which artifact your tooling treats as the source of truth. If those are different files, you've found the direction your drift runs.

None of this applies to a service whose spec nobody reads — generate away; an unreviewed contract has no authority to protect. And the setup has a hole I want to name rather than hide: the CI validates that the yaml is well-formed, not that it still tells the truth about the code. Structural drift is gated; semantic drift is still caught by me, on a manual read, or not at all. I know which direction the drift runs now. I still don't have a machine that notices when it arrives.

Share: