Versioning & Evolution
The longest-lived part of the contract. Additive change, enum evolution, deprecation as a process, consumer telemetry before removal, schema-first vs code-first, docs and SDKs.
Every lesson below names the consumers, the design question and the guarantee before recommending anything. Recommendations come with what they cost, when not to use them, and how they evolve.
URI versions, header versions, date-pinned versions, or no versions at all — the strategies differ less than the arguments suggest. What matters is what a version promises, what minting one costs, and why additive evolution is the strategy every good API uses between versions.
The safe list and the breaking list are shorter and stranger than intuition says. Adding an optional field is safe; making an optional field required is not; tightening validation, changing a default, or changing what a value means breaks clients without touching a single field name.
You add `suspended` to a status enum — additive, surely safe. Every old client that switched exhaustively over the closed set now throws, hides the record, or worse, treats it as `active`. Enums are the sharpest edge of compatibility, and the fix is a contract clause, not a code change.
Addition is a deploy; removal is a program. Introduce the replacement, measure who still reads the old field, deprecate it visibly, run a real migration window, and remove only when telemetry — not hope — says zero. The steps are boring; skipping any of them is an outage.
Marking something deprecated changes nothing; deprecation is a campaign with artifacts — announcement, migration guide, machine-readable signals, telemetry, a deadline someone will enforce — and a finish line. A deprecation nobody plans to complete is just an apology in advance.
"Can we remove this?" is a telemetry query, not a debate. Per-consumer, per-field usage attribution turns evolution decisions from opinions into evidence — and the 12% of mobile users on an old build stop being invisible exactly when you can count them.
Every breaking change, whatever its label, runs the same program: ship the new surface, support both, move consumers with telemetry and deadlines, deprecate, remove. The compatibility matrix — which client works against which API — is the map; the burn-down is the engine.
Whether the contract file or the handler code comes first matters less than which one is the enforced source of truth. Schema-first buys review-before-build and cross-team parallelism; code-first buys iteration speed; drift — where the served API and the described API diverge — is the failure mode both must engineer away.
OpenAPI captures paths, operations, schemas and security schemes in a machine-readable file — which earns you linting, diffing, mocks, generated clients and always-current reference docs. What it cannot capture is most of what this domain teaches: guarantees live in prose, and the spec is the skeleton they hang on.
For every consumer you never meet, the docs are the API. What must be documented is exactly what consumers are forced to assume — auth, errors, pagination, rate limits, idempotency, guarantees — and the examples are the most-executed code you ship. Undocumented behavior gets reverse-engineered and depended on anyway.
`payments.create({...})` versus hand-rolled HTTP is the visible part. The invisible part is what the SDK owns on behalf of every consumer — retries with idempotency keys, pagination iterators, typed errors, timeouts — and how it is built to survive the API evolving underneath it. A strict SDK turns your safe changes into their crashes.