API Versioning Strategies: Managing API Evolution Without Breaking Clients
Learn different API versioning strategies including URL versioning, header versioning, and query parameters. Understand backward compatibility and deprecation practices.
API Versioning Strategies: Managing API Evolution Without Breaking Clients
APIs change. You add features, rename fields, split resources, and sometimes fix mistakes. The challenge is making these changes without breaking existing clients.
Versioning gives you a way to introduce changes while maintaining backward compatibility with older clients.
The RESTful API Design post covers REST basics. This post focuses on versioning strategies.
Why Version?
APIs serve multiple clients simultaneously. A mobile app from 6 months ago still needs to work. Web clients deployed last week need to work. New clients can use new features.
Without versioning, a breaking change affects all clients at once. With versioning, you introduce changes in a new version while keeping the old version running temporarily.
graph LR
A[Old Client] -->|Uses v1| B[API v1]
C[New Client] -->|Uses v2| D[API v2]
D --> E[New Features]
B --> F[Stable Old API]
Versioning Strategies
There are three main approaches to API versioning.
URL Path Versioning
The version appears in the URL path:
GET /v1/users
GET /v2/users
POST /v1/users
POST /v2/users
This is the most common approach. It is explicit and easy to test.
URL path versioning is explicit and easy to test. Load balancers route by URL prefix, and different versions cache separately. The downside is the version appears in every endpoint, and some argue it violates REST principles about resource URLs staying stable.
Header Versioning
The version appears in a request header:
GET /users
Accept: application/vnd.example.v2+json
GET /users
API-Version: 2023-01-01
Header versioning keeps URLs clean and supports content negotiation. The trade-offs are that the version is less visible, testing requires header setup, and caching gets complicated when the same URL returns different results based on headers.
Query Parameter Versioning
The version appears as a query parameter:
GET /users?version=2
GET /users?v=2
Query parameter versioning keeps URLs readable and is easy to add and test. The downside is that parameters can get lost in logs, proxies sometimes strip query params, and it is less explicit than URL versioning.
URL Path Versioning in Practice
Most companies use URL path versioning. It is the simplest to implement and understand.
# Structure
/v{version}/{resource}
/v1/users
/v2/users
/v3/orders
# Full examples
GET /v1/users/123
POST /v2/orders
PUT /v3/products/456
Routing Implementation
// Express.js routing with versioning
app.use("/v1", require("./routes/v1"));
app.use("/v2", require("./routes/v2"));
app.use("/v3", require("./routes/v3"));
API Gateway
# API Gateway routing
- path: /v1/*
target: backend-v1
- path: /v2/*
target: backend-v2
- path: /v3/*
target: backend-v3
Backward Compatibility
Versioning is not a license to make breaking changes. Try to stay backward compatible within a version.
What Breaks Clients
These changes break existing clients:
- Removing fields from responses
- Changing field types
- Renaming fields
- Changing field meaning
- Changing authentication requirements
- Changing required parameters
What Does Not Break Clients
These changes are backward compatible:
- Adding new optional fields to responses
- Adding new optional parameters to requests
- Adding new endpoints
- Reordering fields in responses (if clients do not depend on order)
Deprecation Practices
When you must make breaking changes:
- Announce deprecation in documentation
- Add deprecation headers in responses
- Support old version for a transition period (typically 6-12 months)
- Monitor usage of old versions
- Sunset the old version
# Deprecation response header
HTTP/1.1 200 OK
Deprecation: true
Sunset: Sat, 31 Dec 2026 23:59:59 GMT
Link: <https://api.example.com/v2/users>; rel="successor-version"
Version Lifecycle
A typical version lifecycle:
graph LR
A[Introduce v1] --> B[Monitor Usage]
B --> C[v2 Released]
C --> D[v1 Deprecated]
D --> E[v1 Usage Low]
E --> F[v1 Sunset]
F --> G[v1 Removed]
Timeline Example
| Phase | Duration | Notes |
|---|---|---|
| New version introduced | - | Both v1 and v2 available |
| Old version deprecated | 6 months | Deprecation header added |
| Old version sunset announced | 3 months | Usage monitored |
| Old version sunset | - | Still available at reduced SLA |
| Old version removed | - | Only new version remains |
Multi-Version Deployment
Running multiple versions simultaneously requires planning.
Shared Database
graph TD
A[v1 Code] --> B[Shared Database]
A2[v2 Code] --> B
B --> C[Schema Migration]
Both versions use the same database. Schema changes must support both versions during transition.
Version-Specific Transformations
// Transform response based on version
function userResponse(user, version) {
if (version === "v1") {
return {
id: user.id,
name: user.full_name,
email: user.email_address,
};
}
return {
id: user.id,
name: user.name,
email: user.email,
preferences: user.preferences,
};
}
Best Practices
Key practices: start with v1 from day one. Use clear version formats like v1, v2 or dates. Document breaking changes explicitly. Give deprecation warnings early through headers, documentation, and emails. Set clear sunset dates. Monitor version usage. Keep transition periods reasonable (6-12 months). Do not rush to deprecate.
When to Version
Not every change requires a new version.
Version When
- Changing field names
- Changing field types
- Removing fields
- Changing authentication
- Changing required parameters
- Removing endpoints
Do Not Version When
- Adding new optional fields
- Adding new endpoints
- Adding new optional parameters
- Fixing bugs that were always wrong
- Performance improvements
Production Failure Scenarios
| Failure | Impact | Mitigation |
|---|---|---|
| Breaking change without new version | Existing clients break | Never make breaking changes; always increment version |
| Old version sunset too soon | Client applications fail | Monitor usage before sunset; give adequate notice (6-12 months) |
| Version coexistence bugs | Data inconsistency between versions | Test all version combinations; use transformation layers |
| Shared database schema conflicts | Version N breaks if version N+1 changes schema | Use database abstraction; migrations must support all versions |
| Incorrect version routing | Wrong version serves request; wrong data | Implement routing tests; monitor version distribution |
| Missing deprecation headers | Clients do not know version is deprecated | Always include Deprecation and Sunset headers |
| Version proliferation | Too many versions to maintain | Set clear sunset policies; consolidate versions |
| Side-by-side deployment complexity | Operational overhead increases | Automate version deployment; use API gateway |
Observability Checklist
Metrics
- Request rate by API version
- Error rate by API version
- Latency by API version
- Version distribution (percentage of traffic per version)
- Client SDK distribution by version
- Version-specific feature usage
- Deprecation timeline adherence
Logs
- Requests with version identifier (explicit in URL or header)
- Client identification (User-Agent, API key)
- Errors with version context
- Breaking change attempts
- Deprecation warnings served
- Version migration events
Alerts
- Old version usage exceeds threshold (e.g., > 10% on deprecated version)
- Version usage drops unexpectedly (migration issues)
- Error rate spikes on any version
- Deprecation timeline approaching
- New version adoption slower than expected
- Unauthorized access attempts on old versions
Security Checklist
- Apply same authentication and authorization to all versions
- Do not reduce security on old versions (maintain parity)
- Audit which versions have which security features
- Remove vulnerable endpoints across all versions
- Implement rate limiting per version
- Log access to all versions for compliance
- Monitor for version-specific attacks
- Keep security patches applied to all versions
- Review third-party access by version
Common Pitfalls / Anti-Patterns
Making Breaking Changes Within a Version
Never break existing clients within a version.
# Version 1.0 - Original
GET /v1/users/123
# Returns: { "id": 123, "name": "Alice", "email": "alice@example.com" }
# WRONG - Breaking change in v1
GET /v1/users/123
# Now returns: { "id": 123, "full_name": "Alice Smith" }
# Old clients cannot parse "full_name"
# CORRECT - New version for breaking change
GET /v2/users/123
# Returns: { "id": 123, "name": "Alice Smith", "email": "alice@example.com" }
Ignoring Deprecation Communication
Clients cannot adapt if they do not know a version is going away.
# Always include deprecation headers
HTTP/1.1 200 OK
Deprecation: true
Sunset: Sat, 31 Dec 2026 23:59:59 GMT
Link: <https://api.example.com/v2/users>; rel="successor-version"
Setting Sunset Dates Too Soon
Short deprecation periods cause client breakage.
# Bad: Too short notice
Deprecation: true
Sunset: Tue, 31 Mar 2026 # Only 1 week notice!
# Good: Reasonable transition period
Deprecation: true
Sunset: Sat, 31 Dec 2026 # 9 months notice
Not Monitoring Version Usage
You cannot sunset safely without knowing who uses each version.
// Track version usage
app.use("/v1/*", (req, res, next) => {
metrics.increment("api.v1.requests", { endpoint: req.path });
next();
});
Quick Recap
Key Bullets
- Versioning enables APIs to evolve without breaking existing clients
- URL path versioning (/v1/users) is most explicit and commonly used
- Header versioning keeps URLs clean but is harder to test and cache
- Query parameter versioning is simple but less visible
- Within a version: add optional fields, never remove or rename
- Breaking changes require a new version: removing fields, changing types, changing semantics
- Always communicate deprecation early with headers and documentation
- Monitor version usage before sunsetting old versions
- Give clients 6-12 months to migrate before removing versions
- Use deprecation headers: Deprecation, Sunset, Link (successor-version)
Copy/Paste Checklist
# Check which version is being used (header versioning)
curl -X GET https://api.example.com/users \
-H "API-Version: 2023-01-01"
# Monitor API version distribution
# Use analytics dashboards or log analysis
grep -oE '"version":"[^"]*"' access.log | sort | uniq -c
# Test specific version
curl -X GET https://api.example.com/v1/users/123
curl -X GET https://api.example.com/v2/users/123
# Check deprecation headers
curl -I https://api.example.com/v1/users 2>/dev/null | grep -iE "(deprecat|sunset|link)"
# Version routing example (nginx)
location /v1/ {
proxy_pass http://backend-v1;
add_header Deprecation "true";
}
location /v2/ {
proxy_pass http://backend-v2;
}
Conclusion
API versioning is about managing change without breaking clients. URL path versioning is the most common approach because it is explicit and easy to implement. Header versioning keeps URLs clean but is harder to test. Query parameter versioning is simple but less visible.
Good versioning practices include deprecating clearly, giving long transition periods, and monitoring usage. Within a version, avoid breaking changes. Only create a new version when you need to make changes that would break existing clients.
For REST fundamentals, see the RESTful API Design post. For comparing API styles, see the GraphQL vs REST post.
Category
Related Posts
GraphQL vs REST: Choosing the Right API Paradigm
Compare GraphQL and REST APIs, understand when to use each approach, schema design, queries, mutations, and trade-offs between the two paradigms.
RESTful API Design: Best Practices for Building Web APIs
Learn REST principles, resource naming, HTTP methods, status codes, and best practices. Design clean, maintainable, and scalable RESTful APIs.
Rate Limiting: Token Bucket, Sliding Window, and Distributed
Rate limiting protects APIs from abuse. Learn token bucket, sliding window, fixed window algorithms and distributed rate limiting at scale.