Versioning Strategies
Series
API Design MasteryEvery API will eventually need to change in a breaking way. The question is not whether, but when — and whether your versioning strategy gives clients enough runway to adapt without firefighting on both ends. Most teams reach for URL versioning by default, ship it, and later discover its hidden costs. Understanding all three approaches before you commit can save months of pain.
What Counts as a Breaking Change
Before picking a strategy, agree on what triggers a new version. A non-exhaustive list:
- Removing or renaming a field
- Changing a field's type (string → integer)
- Making an optional field required
- Removing an endpoint
- Changing authentication scheme
- Altering error response shape
Additive changes — new optional fields, new endpoints, new enum values your clients are told to ignore — are generally safe without a version bump.
URL Versioning
The most visible and most widely adopted approach. Version lives in the path:
GET /v1/orders/42
GET /v2/orders/42Pros: trivially cacheable, easy to test in a browser, obvious in logs.
Cons: the URL should identify a resource, not an API contract epoch. You end up with two URLs pointing at the same logical resource. Documentation, links, and bookmarks silently rot when you retire /v1.
GET /v2/orders/42 HTTP/1.1
Host: api.example.com
Authorization: Bearer eyJ...URL versioning works well when your clients are third-party developers who paste URLs into config files. It fails when you have many internal services calling each other, because every service must be updated when you retire a version.
Header Versioning
Version travels in a request header, leaving the URL stable:
GET /orders/42 HTTP/1.1
Host: api.example.com
API-Version: 2025-10-01
Authorization: Bearer eyJ...Date-based versions (used by Stripe, Twilio) are more expressive than integers: 2025-10-01 tells you exactly when the contract was introduced. Each client is pinned to the version active when it was written; you can advance their pin deliberately during upgrades.
Pros: resource identity is stable, date-based versions carry semantic meaning, easy to add version pinning per API key.
Cons: harder to test with curl by default, caching requires Vary: API-Version on CDN/proxy layers.
HTTP/1.1 200 OK
Content-Type: application/json
API-Version: 2025-10-01
Sunset: Sat, 01 Apr 2026 00:00:00 GMTContent Negotiation
Version is embedded in the Accept media type, following RFC 6838:
GET /orders/42 HTTP/1.1
Accept: application/vnd.example.order+json;version=2This is the most "correct" from a REST purist's perspective — resources are negotiated by representation type, which is exactly what Accept is for.
Pros: extremely precise, lets one endpoint serve multiple representation formats simultaneously.
Cons: verbose, most HTTP clients handle this poorly out of the box, middleware and API gateways rarely index on Accept for routing, and debugging requires inspecting headers carefully.
Few public APIs use this successfully. It shines in internal hypermedia APIs and document-oriented systems.
Choosing a Strategy
| Factor | URL | Header | Content Negotiation |
|---|---|---|---|
| Third-party developers | Excellent | Good | Poor |
| Internal microservices | Acceptable | Excellent | Acceptable |
| CDN cacheability | Excellent | Requires Vary | Requires Vary |
| Browser testable | Yes | No | No |
| Resource URL stability | Poor | Excellent | Excellent |
A practical recommendation: use URL versioning for your public API (developer experience wins), and date-based header versioning for internal APIs where URL stability matters and you control all clients.
Running Two Versions Simultaneously
Regardless of strategy, you will run multiple versions in parallel. Keep that window as short as possible.
Route at the gateway level, not in application code:
# nginx example
location /v1/ {
proxy_pass http://api-v1-service/;
}
location /v2/ {
proxy_pass http://api-v2-service/;
}This keeps version branching out of your business logic and lets you kill v1 by updating a config file.
Deprecation Signaling
Send signals early and repeatedly:
HTTP/1.1 200 OK
Deprecation: true
Sunset: Sat, 01 Apr 2026 00:00:00 GMT
Link: <https://api.example.com/v2/orders/42>; rel="successor-version"The Sunset header (RFC 8594) and Deprecation header (draft-ietf-httpapi-deprecation-header) are standard. Log usage of deprecated endpoints per client ID so you can reach out to stragglers before the sunset date.
Key Takeaways
- Define what counts as a breaking change before your first public release; ambiguity creates conflict later.
- URL versioning wins on developer experience and CDN compatibility; use it for public APIs.
- Date-based header versioning is more expressive and keeps resource URLs stable; use it for internal service APIs.
- Content negotiation is theoretically correct but practically painful for most teams.
- Run v1 and v2 in parallel at the gateway layer, not inside application code.
- Send
SunsetandDeprecationheaders from the moment you ship a new version, and monitor per-client usage of old endpoints to manage the rolloff actively.
Series
API Design Mastery