API Versioning Deep Dive: Complete Guide
API versioning is crucial for evolving your API without breaking existing clients. This guide covers different versioning strategies, implementation approaches, and deprecation best practices.
Why API Versioning Matters
- Allow API changes without breaking clients
- Support multiple API versions simultaneously
- Give clients time to migrate
- Maintain backward compatibility
- Enable gradual rollouts
Versioning Strategies
1. URL Path Versioning (Most Common)
# Version in URL path
GET /v1/users
GET /v2/users
GET /v3/users
Pros:
- Easy to understand
- Simple to test
- Clear which version is being used
Cons:
- URL proliferation
- Harder to maintain single endpoint
2. Header Versioning
# Custom header
GET /users
X-API-Version: 2024-01-01
# Accept header
GET /users
Accept: application/vnd.myapp.v2+json
Pros:
- Cleaner URLs
- Multiple versions on same URL
Cons:
- Less visible
- Harder to test
3. Query Parameter Versioning
GET /users?version=2
Pros:
- Simple to implement
- Easy to test
Cons:
- Caching issues
- Can be forgotten
Implementation Examples
URL Path Versioning (Express.js)
const express = require('express');
const app = express();
// V1 routes
app.get('/v1/users', (req, res) => {
res.json({
users: db.users.map(u => ({
id: u.id,
name: u.name,
email: u.email
}))
});
});
// V2 routes
app.get('/v2/users', (req, res) => {
res.json({
users: db.users.map(u => ({
id: u.id,
name: u.name,
email: u.email,
profile: u.profile,
preferences: u.preferences
}))
});
});
// Latest version redirect
app.get('/users', (req, res) => {
res.redirect('/v2/users');
});
Version Router Pattern
const v1Routes = require('./routes/v1');
const v2Routes = require('./routes/v2');
const apiRouter = express.Router();
apiRouter.use('/v1', v1Routes);
apiRouter.use('/v2', v2Routes);
app.use('/api', apiRouter);
// Handle unknown versions
app.use('/api/*', (req, res) => {
res.status(404).json({
error: {
code: 'VERSION_NOT_FOUND',
message: 'API version not supported',
supportedVersions: ['v1', 'v2']
}
});
});
Header Versioning
const versionMiddleware = (req, res, next) => {
// Check header
const version = req.headers['x-api-version'] || 'v1';
// Validate version
if (!['v1', 'v2', 'v3'].includes(version)) {
return res.status(400).json({
error: {
code: 'INVALID_VERSION',
message: 'Invalid API version',
supported: ['v1', 'v2', 'v3']
}
});
}
req.apiVersion = version;
next();
};
app.use('/api', versionMiddleware);
app.get('/api/users', (req, res) => {
const handlers = {
v1: getUsersV1,
v2: getUsersV2,
v3: getUsersV3
};
return handlers[req.apiVersion](req, res);
});
Response Format by Version
V1 Response
{
"users": [
{
"id": "123",
"name": "John Doe",
"email": "[email protected]"
}
]
}
V2 Response (Breaking Changes)
{
"data": [
{
"id": "123",
"type": "user",
"attributes": {
"name": "John Doe",
"email": "[email protected]"
},
"meta": {
"created": "2024-01-01T00:00:00Z"
}
}
],
"meta": {
"version": "v2",
"total": 1
}
}
Deprecation Strategy
Deprecation Headers
app.get('/v1/users', (req, res) => {
// Mark as deprecated
res.set('Deprecation', 'true');
res.set('Sunset', 'Sat, 01 Jan 2025 00:00:00 GMT');
res.set('Link', '<https://api.example.com/v2/users>; rel="successor-version"');
res.json({ /* ... */ });
});
Deprecation Response
{
"users": [],
"deprecation": {
"version": "v1",
"sunsetDate": "2025-01-01",
"message": "v1 will be discontinued",
"migrationGuide": "https://api.example.com/docs/v1-to-v2",
"successorVersion": "v2"
}
}
Deprecation Schedule
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
โ Deprecation Timeline โ
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโค
โ โ
โ v1.0 โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ> โ
โ โ โ
โ โ Announced Sunset End of Life โ
โ โ โ โ โ โ
โ โผ โผ โผ โผ โ
โ โโโโโโโโฌโโโโโโโโโโโโโโโโฌโโโโโโโโโโโโโโโโฌโโโโโโโโโโโโ โ
โ โ โ โ โ
โ โ โ Support โ โ Deprecated โ โ Removed โ
โ โ โ โ โ
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
Version Negotiation
Auto-Deprecation with Warnings
app.use((req, res, next) => {
const version = req.headers['x-api-version'];
// Warn about old versions
if (version === 'v1') {
res.set('Warning', '299 - "v1 is deprecated, please migrate to v2"');
}
if (!version) {
// Default to latest version but inform client
req.apiVersion = 'v2';
res.set('X-API-Deprecation', 'true');
res.set('X-API-Sunset-Date', '2025-01-01');
}
next();
});
Feature Flags by Version
const features = {
v1: ['basic_users', 'read_only'],
v2: ['basic_users', 'full_profiles', 'preferences'],
v3: ['basic_users', 'full_profiles', 'preferences', 'analytics', 'export']
};
app.get('/api/features', (req, res) => {
const version = req.apiVersion;
res.json({
version,
features: features[version] || features.v3
});
});
Best Practices
Version Naming
// Good: Semantic versioning
v1.0.0 - First stable release
v1.1.0 - Backward compatible features
v1.2.0 - Backward compatible features
v2.0.0 - Breaking changes
v2.1.0 - Backward compatible
// Good: Date-based versioning
v2024-01-01
v2024-06-01
// Bad: No clear versioning
latest
new
stable
Migration Guide
# Migration Guide: v1 to v2
## Breaking Changes
1. **Response Format**
- v1: `{ "users": [...] }`
- v2: `{ "data": [...], "meta": {...} }`
2. **Field Renamed**
- v1: `username`
- v2: `login`
3. **Field Removed**
- Removed: `last_login`
- Use: `last_seen`
4. **Authentication**
- v1: API Key in header
- v2: Bearer token required
## How to Migrate
1. Update endpoint URLs from `/v1/` to `/v2/`
2. Update response parsing for new format
3. Replace `username` with `login`
4. Update authentication headers
Multi-Version Support
Service Implementation
// Separate handlers for each version
const handlers = {
v1: {
list: (req, res) => {
// Simple response
res.json({ users: db.users.map(u => u.name) });
},
get: (req, res) => {
const user = db.users.find(req.params.id);
res.json(user);
}
},
v2: {
list: (req, res) => {
// Enhanced response with pagination
const page = parseInt(req.query.page) || 1;
const limit = parseInt(req.query.limit) || 20;
res.json({
data: db.users.map(u => ({ ...u, profile: u.profile })),
meta: { page, limit, total: db.users.length }
});
},
get: (req, res) => {
const user = db.users.find(req.params.id);
res.json({
data: user,
included: [user.profile, user.preferences]
});
}
}
};
// Route dispatcher
app.get('/users', (req, res) => {
const handler = handlers[req.apiVersion]?.list || handlers.v2.list;
return handler(req, res);
});
Comments