API versioning and deprecation
Most API change can be additive and never needs a new version. When a breaking change is genuinely required, version it deliberately and run a disciplined, header-driven deprecation lifecycle so consumers get a removal date and a migration window — never a silent break.
Evolve additively first
- You MUST treat additive changes (new optional fields, new endpoints, new optional query params, new enum values consumers can ignore) as non-breaking and ship them without a version bump.
- You MUST treat these as breaking and requiring a version: removing or renaming a field, changing a type or units, tightening validation, changing defaults, altering status codes, or changing pagination/auth semantics.
- Clients MUST tolerate unknown fields (ignore, don't reject) so the server can add fields freely. Per the observable-behavior-contract guideline (Hyrum's Law), assume consumers depend on every observable detail — undocumented field order, error text, timing — so changing those can break someone even when the spec did not.
Choosing a versioning scheme (a decision, not a mandate)
Pick ONE scheme and apply it consistently across the whole surface. Each is a deliberate trade-off:
| Scheme | Form | Trade-off |
|---|---|---|
| URI path | /v2/orders |
Most visible and cacheable; couples version to URL, harder for fine-grained evolution |
| Media-type / header | Accept: application/vnd.acme.v2+json |
Keeps URLs stable, content-negotiation friendly; less obvious, easy to omit |
| Query param | /orders?version=2 |
Simple; pollutes URLs and caches, easy to forget |
- You MUST NOT mix schemes within one API.
- You SHOULD version at a coarse grain (major version per breaking batch), not per endpoint or per field.
- Unversioned requests SHOULD resolve to a documented, pinned default version rather than "latest", so default behavior cannot shift under a client.
Deprecation lifecycle
When retiring a version or endpoint, run this sequence and publish the timeline before the first signal ships:
- Announce — set the
Deprecationresponse header (RFC 9745, published March 2025) on affected responses. Value is the deprecation timestamp ortrue. You SHOULD add aLinkheader withrel="deprecation"andrel="successor-version"pointing at migration docs. - Set a removal date — add the
Sunsetheader (RFC 8594) with the HTTP-date after which behavior is undefined. TheSunsettime MUST NOT be earlier than theDeprecationtime. - Document — record the change, replacement, and dates in a public changelog and the OpenAPI/spec (
deprecated: true). - Honor a migration window — give consumers a published, generous window before removal; do not shorten it after announcement.
Deprecation: @1717200000
Sunset: Wed, 31 Mar 2027 23:59:59 GMT
Link: <https://api.acme.com/docs/migrate-v2>; rel="successor-version"
- Breaking changes MUST be versioned, never shipped in place.
- Deprecations SHOULD use
Deprecation+Sunsetheaders plus a changelog entry; headers are hints, so they MUST be paired with documentation, not used alone. - You SHOULD emit metrics on deprecated-version traffic and notify identifiable high-volume consumers directly before removal.
Anti-patterns
- Silently changing behavior of an existing version (breaks the observable-behavior-contract).
- Removing an endpoint with no
Sunsetdate or before the announced window. - Auto-upgrading unversioned clients to "latest".
- A new major version per trivial change, multiplying surfaces you must maintain.