| name | api-versioning-strategy |
| description | Implement API versioning strategies including URL versioning, header versioning, backward compatibility, deprecation strategies, and migration guides. Use when dealing with API versions, deprecating endpoints, or managing breaking changes. |
API Versioning Strategy
Overview
Comprehensive guide to API versioning approaches, deprecation strategies, backward compatibility techniques, and migration planning for REST APIs, GraphQL, and gRPC services.
When to Use
- Designing new APIs with versioning from the start
- Adding breaking changes to existing APIs
- Deprecating old API versions
- Planning API migrations
- Ensuring backward compatibility
- Managing multiple API versions simultaneously
- Creating API documentation for different versions
- Implementing API version routing
Instructions
1. Versioning Approaches
URL Path Versioning
// express-router.ts
import express from 'express';
const app = express();
// Version 1
app.get('/api/v1/users', (req, res) => {
res.json({
users: [
{ id: 1, name: 'John Doe' }
]
});
});
// Version 2 - Added email field
app.get('/api/v2/users', (req, res) => {
res.json({
users: [
{ id: 1, name: 'John Doe', email: 'john@example.com' }
]
});
});
// Shared logic with version-specific transformations
app.get('/api/:version/users/:id', async (req, res) => {
const user = await userService.findById(req.params.id);
if (req.params.version === 'v1') {
res.json({ id: user.id, name: user.name });
} else if (req.params.version === 'v2') {
res.json({ id: user.id, name: user.name, email: user.email });
}
});
Pros: Simple, explicit, cache-friendly Cons: URL pollution, harder to deprecate
Header Versioning (Content Negotiation)
// header-versioning.ts
app.get('/api/users', (req, res) => {
const version = req.headers['api-version'] || '1';
switch (version) {
case '1':
return res.json(transformToV1(users));
case '2':
return res.json(transformToV2(users));
default:
return res.status(400).json({ error: 'Unsupported API version' });
}
});
// Or using Accept header
app.get('/api/users', (req, res) => {
const acceptHeader = req.headers['accept'];
if (acceptHeader.includes('application/vnd.myapi.v2+json')) {
return res.json(transformToV2(users));
}
// Default to v1
return res.json(transformToV1(users));
});
Pros: Clean URLs, RESTful Cons: Less visible, harder to test manually
Query Parameter Versioning
// query-param-versioning.ts
app.get('/api/users', (req, res) => {
const version = req.query.version || '1';
if (version === '2') {
return res.json(transformToV2(users));
}
return res.json(transformToV1(users));
});
// Usage: GET /api/users?version=2
Pros: Easy to implement, flexible Cons: Not RESTful, can be overlooked
2. Backward Compatibility Patterns
Additive Changes (Non-Breaking)
// ✅ Safe: Adding optional fields
interface UserV1 {
id: string;
name: string;
}
interface UserV2 extends UserV1 {
email?: string; // Optional field
avatar?: string; // Optional field
}
// ✅ Safe: Adding new endpoints
app.post('/api/v1/users/:id/avatar', uploadAvatar);
// ✅ Safe: Accepting additional parameters
app.get('/api/v1/users', (req, res) => {
const { page, limit, sortBy } = req.query; // New optional params
const users = await userService.list({ page, limit, sortBy });
res.json(users);
});
Breaking Changes (Require New Version)
// ❌ Breaking: Removing fields
interface UserV1 {
id: string;
name: string;
username: string;
}
interface UserV2 {
id: string;
name: string;
// username removed - BREAKING!
}
// ❌ Breaking: Changing field types
interface UserV1 {
id: string;
created: string; // ISO string
}
interface UserV2 {
id: string;
created: number; // Unix timestamp - BREAKING!
}
// ❌ Breaking: Renaming fields
interface UserV1 {
fullName: string;
}
interface UserV2 {
name: string; // Renamed from fullName - BREAKING!
}
// ❌ Breaking: Changing response structure
// V1
{ users: [...], total: 10 }
// V2 - BREAKING!
{ data: [...], meta: { total: 10 } }
Handling Both Versions
// version-adapter.ts
export class UserAdapter {
toV1(user: User): UserV1Response {
return {
id: user.id,
name: user.fullName,
username: user.username,
created: user.createdAt.toISOString()
};
}
toV2(user: User): UserV2Response {
return {
id: user.id,
name: user.fullName,
email: user.email,
profile: {
avatar: user.avatarUrl,
bio: user.bio
},
createdAt: user.createdAt.getTime()
};
}
fromV1(data: UserV1Request): User {
return {
fullName: data.name,
username: data.username,
email: data.email || null
};
}
fromV2(data: UserV2Request): User {
return {
fullName: data.name,
username: data.username || generateUsername(data.email),
email: data.email,
avatarUrl: data.profile?.avatar,
bio: data.profile?.bio
};
}
}
// Usage in controller
app.get('/api/:version/users/:id', async (req, res) => {
const user = await userService.findById(req.params.id);
const adapter = new UserAdapter();
const response = req.params.version === 'v2'
? adapter.toV2(user)
: adapter.toV1(user);
res.json(response);
});
3. Deprecation Strategy
Deprecation Headers
// deprecation-middleware.ts
export function deprecationWarning(version: string, sunsetDate: Date) {
return (req, res, next) => {
res.setHeader('Deprecation', 'true');
res.setHeader('Sunset', sunsetDate.toUTCString());
res.setHeader('Link', '</api/v2/docs>; rel="successor-version"');
res.setHeader('X-API-Warn', `Version ${version} is deprecated. Please migrate to v2 by ${sunsetDate.toDateString()}`);
next();
};
}
// Apply to deprecated routes
app.use('/api/v1/*', deprecationWarning('v1', new Date('2024-12-31')));
app.get('/api/v1/users', (req, res) => {
// Return v1 response with deprecation headers
res.json(users);
});
Deprecation Response
// Include deprecation info in response body
app.get('/api/v1/users', (req, res) => {
res.json({
_meta: {
deprecated: true,
sunsetDate: '2024-12-31',
message: 'This API version is deprecated. Please migrate to v2.',
migrationGuide: 'https://docs.example.com/migration-v1-to-v2'
},
users: [...]
});
});
Gradual Deprecation Timeline
// deprecation-stages.ts
enum DeprecationStage {
SUPPORTED = 'supported',
DEPRECATED = 'deprecated',
SUNSET_ANNOUNCED = 'sunset_announced',
READONLY = 'readonly',
SHUTDOWN = 'shutdown'
}
const versionStatus = {
'v1': {
stage: DeprecationStage.READONLY,
sunsetDate: new Date('2024-06-30'),
message: 'Read-only mode. New writes are disabled.'
},
'v2': {
stage: DeprecationStage.DEPRECATED,
sunsetDate: new Date('2024-12-31'),
message: 'Deprecated. Please migrate to v3.'
},
'v3': {
stage: DeprecationStage.SUPPORTED,
message: 'Current stable version.'
}
};
// Middleware to enforce deprecation
app.use('/api/:version/*', (req, res, next) => {
const status = versionStatus[req.params.version];
if (!status) {
return res.status(404).json({ error: 'API version not found' });
}
if (status.stage === DeprecationStage.SHUTDOWN) {
return res.status(410).json({ error: 'API version no longer available' });
}
if (status.stage === DeprecationStage.READONLY &&
['POST', 'PUT', 'PATCH', 'DELETE'].includes(req.method)) {
return res.status(403).json({
error: 'API version is read-only',
message: status.message
});
}
// Add deprecation headers
if (status.stage !== DeprecationStage.SUPPORTED) {
res.setHeader('X-API-Deprecated', 'true');
res.setHeader('X-API-Sunset', status.sunsetDate.toISOString());
}
next();
});
4. Migration Guide Example
# API Migration Guide: v1 to v2
## Overview
Version 2 introduces breaking changes to improve consistency and add new features.
**Timeline:**
- 2024-01-01: v2 released
- 2024-06-01: v1 deprecated
- 2024-09-01: v1 read-only
- 2024-12-31: v1 shutdown
## Breaking Changes
### 1. Response Structure
**v1:**
```json
{
"users": [...],
"total": 10,
"page": 1
}
v2:
{
"data": [...],
"meta": {
"total": 10,
"page": 1,
"perPage": 20
}
}
Migration:
// Before
const users = response.users;
const total = response.total;
// After
const users = response.data;
const total = response.meta.total;
2. Date Format
v1: ISO 8601 strings v2: Unix timestamps
Migration:
// Before
const created = new Date(user.created);
// After
const created = new Date(user.created * 1000);
3. Error Format
v1:
{ "error": "User not found" }
v2:
{
"error": {
"code": "USER_NOT_FOUND",
"message": "User not found",
"details": {}
}
}
New Features in v2
Pagination
// v2 supports cursor-based pagination
GET /api/v2/users?cursor=eyJpZCI6MTIzfQ&limit=20
Field Selection
// v2 supports field filtering
GET /api/v2/users?fields=id,name,email
Batch Operations
// v2 supports batch requests
POST /api/v2/batch
{
"requests": [
{ "method": "GET", "path": "/users/1" },
{ "method": "GET", "path": "/users/2" }
]
}
Code Examples
JavaScript/TypeScript
// v1 Client
class ApiClientV1 {
async getUsers() {
const response = await fetch('/api/v1/users');
const data = await response.json();
return data.users;
}
}
// v2 Client
class ApiClientV2 {
async getUsers() {
const response = await fetch('/api/v2/users');
const data = await response.json();
return data.data; // Changed from .users to .data
}
}
Python
# v1
response = requests.get(f"{base_url}/api/v1/users")
users = response.json()["users"]
# v2
response = requests.get(f"{base_url}/api/v2/users")
users = response.json()["data"]
### 5. **GraphQL Versioning**
```typescript
// GraphQL handles versioning differently - through schema evolution
// schema-v1.graphql
type User {
id: ID!
name: String!
username: String!
}
// schema-v2.graphql (deprecated fields)
type User {
id: ID!
name: String!
username: String! @deprecated(reason: "Use email instead")
email: String!
profile: Profile
}
type Profile {
avatar: String
bio: String
}
// Field deprecation in resolver
const resolvers = {
User: {
username: (user) => {
console.warn('username field is deprecated, use email instead');
return user.email;
}
}
};
6. gRPC Versioning
// v1/user.proto
syntax = "proto3";
package user.v1;
message User {
string id = 1;
string name = 2;
}
// v2/user.proto
syntax = "proto3";
package user.v2;
message User {
string id = 1;
string name = 2;
string email = 3;
Profile profile = 4;
}
message Profile {
string avatar = 1;
string bio = 2;
}
// Both versions can coexist
service UserServiceV1 {
rpc GetUser (GetUserRequest) returns (user.v1.User);
}
service UserServiceV2 {
rpc GetUser (GetUserRequest) returns (user.v2.User);
}
7. Version Detection & Routing
// version-router.ts
import express from 'express';
export class VersionRouter {
private versions = new Map<string, express.Router>();
registerVersion(version: string, router: express.Router) {
this.versions.set(version, router);
}
getMiddleware() {
return (req, res, next) => {
// Detect version from multiple sources
const version = this.detectVersion(req);
const router = this.versions.get(version);
if (!router) {
return res.status(400).json({
error: 'Invalid API version',
supportedVersions: Array.from(this.versions.keys())
});
}
// Set version in request for logging
req.apiVersion = version;
// Use versioned router
router(req, res, next);
};
}
private detectVersion(req): string {
// 1. Check URL path
const pathMatch = req.path.match(/^\/api\/v(\d+)\//);
if (pathMatch) return pathMatch[1];
// 2. Check header
if (req.headers['api-version']) {
return req.headers['api-version'];
}
// 3. Check Accept header
const acceptMatch = req.headers['accept']?.match(/application\/vnd\.myapi\.v(\d+)\+json/);
if (acceptMatch) return acceptMatch[1];
// 4. Check query parameter
if (req.query.version) {
return req.query.version;
}
// 5. Default version
return '1';
}
}
// Usage
const versionRouter = new VersionRouter();
versionRouter.registerVersion('1', v1Router);
versionRouter.registerVersion('2', v2Router);
versionRouter.registerVersion('3', v3Router);
app.use('/api', versionRouter.getMiddleware());
8. Testing Multiple Versions
// api-version.test.ts
describe('API Versioning', () => {
describe('v1', () => {
it('should return user with v1 format', async () => {
const response = await request(app)
.get('/api/v1/users/1')
.expect(200);
expect(response.body).toHaveProperty('id');
expect(response.body).toHaveProperty('name');
expect(response.body).not.toHaveProperty('email');
});
});
describe('v2', () => {
it('should return user with v2 format', async () => {
const response = await request(app)
.get('/api/v2/users/1')
.expect(200);
expect(response.body).toHaveProperty('id');
expect(response.body).toHaveProperty('name');
expect(response.body).toHaveProperty('email');
expect(response.body).toHaveProperty('profile');
});
it('should include deprecation headers for v1', async () => {
const response = await request(app)
.get('/api/v1/users/1');
expect(response.headers['deprecation']).toBe('true');
expect(response.headers['sunset']).toBeDefined();
});
});
describe('version negotiation', () => {
it('should use version from header', async () => {
const response = await request(app)
.get('/api/users/1')
.set('API-Version', '2')
.expect(200);
expect(response.body).toHaveProperty('email');
});
it('should default to v1 if no version specified', async () => {
const response = await request(app)
.get('/api/users/1')
.expect(200);
expect(response.body).not.toHaveProperty('email');
});
});
});
Best Practices
✅ DO
- Version from day one (even if v1)
- Document breaking vs non-breaking changes
- Provide clear migration guides with code examples
- Use semantic versioning principles
- Give 6-12 months deprecation notice
- Monitor usage of deprecated APIs
- Send deprecation warnings to API consumers
- Support at least 2 versions simultaneously
- Use adapters/transformers for version logic
- Test all supported versions
- Log which API version is being used
- Provide migration tooling when possible
- Be consistent with versioning approach
❌ DON'T
- Change API behavior without versioning
- Remove versions without notice
- Support too many versions (>3)
- Use different versioning strategies in same API
- Break APIs without incrementing version
- Forget to update documentation
- Deprecate too quickly (<6 months)
- Ignore feedback from API consumers
- Make every change a new version
- Use version numbers inconsistently
Common Patterns
Pattern 1: Version-Agnostic Core
// Core logic remains version-agnostic
class UserService {
async getUser(id: string): Promise<User> {
return this.repository.findById(id);
}
}
// Version-specific adapters
class UserV1Adapter {
transform(user: User): UserV1 { /* ... */ }
}
class UserV2Adapter {
transform(user: User): UserV2 { /* ... */ }
}
Pattern 2: Feature Flags for Gradual Rollout
app.get('/api/v2/users', async (req, res) => {
const user = await userService.getUser(req.params.id);
// Gradual rollout of new feature
if (featureFlags.isEnabled('enhanced-profile', req.user.id)) {
return res.json(transformWithEnhancedProfile(user));
}
return res.json(transformV2(user));
});
Pattern 3: API Version Metrics
// Track usage by version
app.use((req, res, next) => {
const version = detectVersion(req);
metrics.increment('api.requests', { version });
next();
});
Tools & Resources
- OpenAPI/Swagger: API documentation with version support
- Postman: API testing with version management
- API Blueprint: API design with versioning
- Stoplight: API design and documentation
- Kong: API gateway with version routing