You need to make a breaking change to your API. Maybe you're renaming a field, changing response structure, or removing a deprecated endpoint.
How do you roll out the change without breaking existing clients?
You need API versioning. But there are multiple approaches, each with tradeoffs. Let's compare them.
The Three Versioning Strategies
1. URL Versioning
Put the version in the URL path:
GET /v1/pets/123
GET /v2/pets/123
Used by: Stripe, Twitter, GitHub, Twilio
Pros: - Explicit and visible - Easy to test (just change the URL) - Simple to route (different versions can hit different servers) - Works with all HTTP clients - Easy to cache (different URLs = different cache entries)
Cons: - Pollutes URL space - Requires maintaining multiple URL structures - Can't version individual resources differently - Looks less "clean" to REST purists
2. Header Versioning
Put the version in a custom header:
GET /pets/123
API-Version: 2
Or use the Accept header:
GET /pets/123
Accept: application/vnd.petstore.v2+json
Used by: GitHub (also supports URL), Azure, Atlassian
Pros: - Cleaner URLs - Follows REST principles (URLs identify resources, not versions) - Can version individual resources - Supports content negotiation
Cons: - Less visible (hidden in headers) - Harder to test (must set headers) - More complex routing - Caching requires Vary header - Some clients don't support custom headers easily
3. Content Negotiation
Use the Accept header with media types:
GET /pets/123
Accept: application/vnd.petstore.v2+json
Used by: GitHub API (preferred method)
Pros: - Most RESTful approach - Standard HTTP mechanism - Supports multiple formats (JSON, XML) and versions - Fine-grained control
Cons: - Most complex to implement - Requires understanding of media types - Not intuitive for developers - Harder to debug
URL Versioning: The Pragmatic Choice
Most APIs use URL versioning because it's simple and explicit.
Implementation
Put the version as the first path segment:
/v1/pets
/v1/orders
/v1/users
/v2/pets
/v2/orders
/v2/users
Or after the domain:
https://api.petstore.com/v1/pets
https://api.petstore.com/v2/pets
Routing
URL versioning makes routing straightforward:
// Express.js example
app.use('/v1', v1Router);
app.use('/v2', v2Router);
// v1Router
router.get('/pets/:id', getPetV1);
// v2Router
router.get('/pets/:id', getPetV2);
You can even route different versions to different servers:
# nginx config
location /v1/ {
proxy_pass http://api-v1-servers;
}
location /v2/ {
proxy_pass http://api-v2-servers;
}
Versioning Strategy
Major versions only: Use integers (v1, v2, v3) for breaking changes only.
/v1/pets → Original structure
/v2/pets → Breaking changes (renamed fields, different structure)
Don't version minor changes: Additive changes (new fields, new endpoints) don't need a new version.
// v1 response (original)
{
"id": "123",
"name": "Max"
}
// v1 response (with new field - no version bump)
{
"id": "123",
"name": "Max",
"breed": "Golden Retriever" ← New field added
}
Clients ignore fields they don't recognize. Adding fields is backward compatible.
Deprecation timeline: Support old versions for a defined period:
v1: Released 2024-01-01, deprecated 2025-01-01, sunset 2026-01-01
v2: Released 2025-01-01, current version
Give clients 12-24 months to migrate before sunsetting old versions.
Header Versioning: The REST Approach
Header versioning keeps URLs clean but adds complexity.
Implementation
Use a custom header:
GET /pets/123
API-Version: 2
Or the Accept header:
GET /pets/123
Accept: application/vnd.petstore.v2+json
Routing
Header-based routing is more complex:
app.use((req, res, next) => {
const version = req.headers['api-version'] || '1';
if (version === '1') {
req.apiVersion = 'v1';
} else if (version === '2') {
req.apiVersion = 'v2';
} else {
return res.status(400).json({
error: 'Unsupported API version'
});
}
next();
});
router.get('/pets/:id', (req, res) => {
if (req.apiVersion === 'v1') {
return getPetV1(req, res);
} else {
return getPetV2(req, res);
}
});
Caching Considerations
Header versioning requires the Vary header for proper caching:
GET /pets/123
API-Version: 2
Response:
Vary: API-Version
Cache-Control: public, max-age=3600
The Vary header tells caches to store separate entries for different header values.
Without Vary, caches might return v1 responses to v2 requests (or vice versa).
Content Negotiation: The Purist Approach
Content negotiation uses standard HTTP mechanisms but requires more setup.
Implementation
Use media type versioning:
GET /pets/123
Accept: application/vnd.petstore.v2+json
The media type format: - application = top-level type - vnd.petstore = vendor-specific subtype - v2 = version - json = format
Routing
app.use((req, res, next) => {
const accept = req.headers['accept'] || 'application/json';
if (accept.includes('vnd.petstore.v2+json')) {
req.apiVersion = 'v2';
} else if (accept.includes('vnd.petstore.v1+json')) {
req.apiVersion = 'v1';
} else {
req.apiVersion = 'v1'; // Default
}
next();
});
Supporting Multiple Formats
Content negotiation shines when you support multiple formats:
GET /pets/123
Accept: application/vnd.petstore.v2+json
→ Returns JSON
GET /pets/123
Accept: application/vnd.petstore.v2+xml
→ Returns XML
GET /pets/123
Accept: application/vnd.petstore.v2+protobuf
→ Returns Protocol Buffers
Hybrid Approaches
Some APIs combine strategies.
GitHub's Approach
GitHub supports both URL and header versioning:
# URL versioning (deprecated)
GET https://api.github.com/repos/owner/repo
# Header versioning (preferred)
GET https://api.github.com/repos/owner/repo
Accept: application/vnd.github.v3+json
This gives clients flexibility while encouraging the preferred method.
Stripe's Approach
Stripe uses URL versioning but allows header overrides:
# URL version
GET https://api.stripe.com/v1/customers
# Header override
GET https://api.stripe.com/v1/customers
Stripe-Version: 2024-03-01
The header lets clients test new versions without changing URLs.
Choosing Your Strategy
Use URL Versioning If:
- You want simplicity
- Your team is small
- You have few versions
- Caching is important
- You need easy testing
Use Header Versioning If:
- You want clean URLs
- You follow REST strictly
- You need fine-grained versioning
- You have complex routing needs
Use Content Negotiation If:
- You support multiple formats
- You want maximum REST compliance
- You have sophisticated clients
- You need per-resource versioning
Best Practices
1. Version Only Breaking Changes
Don't bump versions for: - New endpoints - New optional fields - New query parameters - Bug fixes
Do bump versions for: - Renamed fields - Removed endpoints - Changed response structures - Different behavior
2. Document Changes Clearly
Maintain a changelog:
## v2.0.0 (2025-01-01)
### Breaking Changes
- Renamed `status` to `petStatus` in Pet resource
- Removed deprecated `/pet/findByStatus` endpoint
- Changed date format from Unix timestamp to ISO 8601
### New Features
- Added `/pets/search` endpoint with advanced filtering
- Added `breed` field to Pet resource
3. Support Multiple Versions
Don't force immediate migration. Support at least two versions: - Current version (v2) - Previous version (v1)
4. Communicate Deprecation
Give advance notice:
GET /v1/pets/123
Response:
Deprecation: true
Sunset: Sat, 01 Jan 2026 00:00:00 GMT
Link: <https://docs.petstore.com/migration/v1-to-v2>; rel="deprecation"
The Sunset header tells clients when the version will be removed.
5. Default to Latest Stable
If no version is specified, use the latest stable version:
GET /pets/123
→ Uses v2 (latest)
GET /v1/pets/123
→ Uses v1 (explicit)
This encourages adoption of new versions while maintaining backward compatibility.
Migration Path
When releasing a new version:
- Announce the new version 3-6 months in advance
- Release v2 while keeping v1 available
- Deprecate v1 after 6-12 months
- Sunset v1 after 12-24 months
- Remove v1 code after sunset
Give clients ample time to migrate. Enterprise clients need longer timelines than consumer apps.
Conclusion
For most APIs, URL versioning is the right choice. It's simple, explicit, and works everywhere.
Use /v1, /v2, /v3 for major versions. Keep URLs clean by versioning only breaking changes. Support multiple versions with clear deprecation timelines.
Header versioning and content negotiation are valid alternatives if you need cleaner URLs or fine-grained control. But they add complexity that most APIs don't need.
Pick a strategy, document it clearly, and stick with it. Consistency matters more than the specific approach.