Versioning and Deprecation — Middle¶
The junior tier answered why an API needs versioning. This tier answers how — the concrete mechanisms you wire into requests, responses, and routing to evolve an API without breaking existing clients, then retire the old surface on a published schedule. Everything here is on-the-wire: the exact headers, the exact URLs, the routing decisions, and the trace of a client that keeps working while it migrates.
Contents¶
- What actually breaks a client
- Semantic versioning applied to APIs
- The four versioning strategies
- Strategy comparison
- Signaling deprecation on the wire
- Rollout and coexistence of v1 and v2
- Traced flow: deprecated call to migration
- Communicating change to consumers
- Checklist
1. What actually breaks a client¶
Before choosing a mechanism, be precise about what constitutes a breaking change, because that is the only thing that forces a new major version. A change breaks a client when a previously valid request stops working, or a previously parsed response can no longer be parsed by code written against the old contract.
Breaking (needs a new major version):
- Removing or renaming a field, endpoint, or query parameter.
- Changing a field's type (
string→object) or its semantics (amountin dollars → cents). - Adding a new required request field or tightening validation.
- Changing default values, status codes, or error shapes clients branch on.
- Changing pagination, ordering, or authentication requirements.
Non-breaking (ship without a version bump):
- Adding a new optional request field.
- Adding a new field to a response (clients must ignore unknown fields — the "tolerant reader" rule).
- Adding a new endpoint or a new enum value clients already treat as opaque.
- Relaxing validation.
The tolerant-reader expectation is a contract you publish: clients must ignore fields they do not recognize. Without it, every additive change becomes breaking and versioning collapses under its own weight.
2. Semantic versioning applied to APIs¶
Semantic Versioning defines MAJOR.MINOR.PATCH:
- MAJOR — incompatible, breaking changes.
- MINOR — backward-compatible additions.
- PATCH — backward-compatible bug fixes.
For a public HTTP API the practical rule is: only the MAJOR number appears in the API surface. Clients pin to v1 or v2, never to v2.4.1. Minor and patch changes are, by definition, backward compatible, so they must roll out transparently under the same major — a client on v2 silently receives new optional fields and fixes with no action required.
v1 → v2 client action required, coexists during migration
v2.3 → v2.4 additive, transparent, no client action
v2.4.0 → v2.4.1 fix, transparent
You still track the full semver internally (in changelogs, release notes, and an X-API-Version response header for observability), but the routable identifier is the major alone. This keeps the URL/header space small and the migration story binary: you are on the old major or the new one.
3. The four versioning strategies¶
All four answer one question — how does the client tell the server which version it wants? — and all four are demonstrated below fetching the same order resource.
3.1 URI path versioning¶
The version is a path segment.
The most visible and the most common. A version is a distinct URL, so it is trivially routable at the gateway, cacheable by any intermediary keyed on URL, and copy-pasteable into a browser or curl. The purist objection is that /v1/orders/42 and /v2/orders/42 are two URLs for what is arguably one resource. In practice, that objection loses to operability.
3.2 Custom header versioning¶
The version travels in a request header, keeping the URL version-free.
HTTP/1.1 200 OK
Content-Type: application/json
X-API-Version: 2
{ "id": 42, "total": 1999, "currency": "USD" }
The URL stays clean and stable. The cost is invisibility — you cannot see the version in a browser address bar or a naive access log, and caches must be told to vary on the header (Vary: X-API-Version) or they will serve a v1 body to a v2 request.
3.3 Accept media-type versioning (content negotiation)¶
The version is a parameter of the Accept media type — the approach HTTP purists prefer because it uses the protocol's built-in negotiation.
HTTP/1.1 200 OK
Content-Type: application/vnd.example.order.v2+json
Vary: Accept
{ "id": 42, "total": 1999, "currency": "USD" }
This is the most RESTful: the client negotiates a representation of one resource at one URL, and the server echoes the chosen type in Content-Type. It is also the least approachable — building the vendor media-type string is fiddly, tooling support is uneven, and you must set Vary: Accept for caches.
3.4 Query-parameter versioning¶
The version is a query-string parameter.
Easy to add and easy to default (omitting version picks a server-chosen default). But it muddies the resource identity — the version is now part of the cache key alongside real filters, it is easy to drop when clients build URLs, and choosing a sensible default silently pins un-versioned callers to whatever "latest" means today, which itself becomes a breaking surface.
4. Strategy comparison¶
| Dimension | URI path (/v2/orders) | Custom header (X-API-Version: 2) | Media type (Accept: …v2+json) | Query param (?version=2) |
|---|---|---|---|---|
| Visibility | High — in the URL | Low — hidden in headers | Low — hidden in headers | Medium — in the URL |
| Cacheability | Simple — URL is the key | Needs Vary: X-API-Version | Needs Vary: Accept | URL-keyed but pollutes key |
| Gateway routing | Trivial (path match) | Requires header inspection | Requires header parsing | Requires query parsing |
| REST purity | Low — many URLs per resource | Medium | High — one URL, negotiated | Low |
| URL cleanliness | Version in every path | Clean, stable URLs | Clean, stable URLs | Cluttered with version |
| Ease for consumers | Highest — obvious | Medium | Lowest — fiddly media type | High |
| Default-version risk | None (explicit) | Possible | Possible | High (easy to omit) |
Practical guidance. For most public HTTP APIs, URI path versioning wins on operability: it is self-evident, trivially routable, and cache-friendly with no extra configuration. Reach for media-type negotiation only when you have a genuinely REST-mature audience and want one stable URL per resource. Whatever you pick, be consistent across the whole surface — mixing strategies is its own breaking change.
5. Signaling deprecation on the wire¶
Deprecation is a state, not an event: an endpoint still works but is scheduled for removal. The client learns this from response headers on every call, so an operator watching traffic — not just someone who reads the changelog — gets the signal.
| Header | Purpose | Example value |
|---|---|---|
Deprecation | Marks the resource as deprecated; value is true or an HTTP-date when it became deprecated. | Deprecation: true |
Sunset | The date/time after which the resource may stop responding. Defined by RFC 8594. | Sunset: Sat, 31 Oct 2026 23:59:59 GMT |
Link | Points to migration docs or the successor resource, using relation types like sunset and successor-version. | Link: <https://api.example.com/docs/v2-migration>; rel="sunset" |
Warning | Human-readable advisory (code 299 = "Miscellaneous persistent warning"). | Warning: 299 - "Deprecated API; migrate to v2 by 2026-10-31" |
A deprecated endpoint returns a normal 200 OK — deprecation must never change the status code, or you break the very clients you are trying to guide. The signal lives entirely in headers:
HTTP/1.1 200 OK
Content-Type: application/json
Deprecation: true
Sunset: Sat, 31 Oct 2026 23:59:59 GMT
Link: <https://api.example.com/docs/v2-migration>; rel="sunset"
Warning: 299 - "Deprecated API; migrate to /v2/orders by 2026-10-31"
{ "id": 42, "total": 1999 }
Key point: Sunset is a promise, not a hope. Publish the date, then enforce it. After the sunset date a request to the retired endpoint should return 410 Gone (the resource is intentionally, permanently removed) rather than a bare 404, so clients can distinguish "retired" from "typo."
Sunset and Deprecation follow the HTTP-date format (RFC 1123), e.g. Sat, 31 Oct 2026 23:59:59 GMT — always in GMT.
6. Rollout and coexistence of v1 and v2¶
During a migration window, v1 and v2 run side by side. The gateway routes each request to the version the client asked for; both versions serve production traffic until v1 sunsets.
Two common topologies:
- Version-aware routing at the gateway. The gateway inspects the version signal (path segment, header, or media type) and forwards to the matching backend or handler. Path versioning makes this a plain prefix match:
/v1/*→ v1 service,/v2/*→ v2 service. - Dual-running within one service. A single service registers both
/v1and/v2handlers. Where the change is a response reshape rather than new logic, the/v1handler often calls the same core code and then adapts the output back to the old shape (an anti-corruption layer at the edge), so business logic exists once and only the serialization differs per version.
Operational discipline during coexistence:
- Track per-version traffic. Emit a metric tagged by version; you cannot sunset v1 until its call volume approaches zero (or you know exactly which named clients remain).
- Identify laggard clients. Correlate v1 calls with API keys / client IDs so you can reach the specific teams that have not migrated.
- Keep the sunset date honest. Extend it only with a public announcement, never silently — a moved deadline that no one hears about is as bad as a broken contract.
7. Traced flow: deprecated call to migration¶
This is the full lifecycle: a client hits the deprecated v1 endpoint, keeps getting valid 200s while receiving the deprecation and sunset signals, then switches to v2.
Walking the numbers:
- The client calls the old endpoint and gets a fully valid
200— nothing has broken. - The gateway routes on the
/v1path prefix. - v1 responds with the body and the four deprecation signals; a well-behaved client library surfaces the
Warningin logs and theSunsetdate to its operators. - A developer, prompted by the signal, follows the
Linkto migration docs and points the client at/v2. - v2 returns the new contract (here, an added
currencyfield), echoingX-API-Version: 2for observability. - Once traffic has moved, v1 volume falls to zero and the sunset can proceed.
- After the published sunset date, any remaining v1 caller gets
410 Gone— an intentional, permanent removal, distinct from a404.
8. Communicating change to consumers¶
On-the-wire signals reach machines and operators; a migration also needs to reach humans, and no single channel is enough:
- Changelog / release notes — the canonical, dated record of what changed and when. Every breaking change and every sunset date lands here first.
- Response headers — the
Deprecation/Sunset/Link/Warningset, so the signal travels with the traffic even to teams who never read the changelog. - Direct outreach — email or ticket to the specific API-key owners still calling v1, especially as the sunset date nears.
- Migration guide — a focused document (the
Linktarget) showing the v1→v2 diff field by field, with before/after request and response examples. - Runtime warnings in SDKs — if you ship client libraries, emit a deprecation warning at call time so it surfaces in the consumer's own logs.
The order of operations for a breaking change is fixed: announce → mark deprecated (headers live) → run in parallel through the sunset window → enforce sunset (410). Never remove before you announce, and never let the announced date pass without either enforcing it or publicly extending it.
9. Checklist¶
- Only the MAJOR version is routable (
v1,v2); minor/patch roll out transparently under the same major. - Additive changes ship without a version bump; clients follow the tolerant-reader rule and ignore unknown fields.
- Pick one versioning strategy and apply it consistently — URI path is the safe default for public HTTP APIs.
- A deprecated endpoint still returns
200; the signal is inDeprecation,Sunset(RFC 8594),Link, andWarningheaders. - Caches must
Varyon any header carrying the version (Vary: X-API-VersionorVary: Accept). - v1 and v2 coexist during the migration window; route by version at the gateway and track per-version traffic.
- After the published
Sunsetdate, return410 Gone, not404. - Communicate through multiple channels; never remove before announcing or move a date silently.
Next step: Versioning and Deprecation — Senior
In this topic
- junior
- middle
- senior
- professional