Introduction
APIs evolve over time. New features are added, fields are deprecated, and breaking changes are sometimes necessary. API versioning allows you to make these changes without breaking existing clients. This guide covers the main versioning strategies and when to use each.
Versioning Strategies Overview
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
โ API Versioning Strategies โ
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโค
โ โ
โ 1. URL Path Versioning โ
โ GET /api/v1/users โ
โ GET /api/v2/users โ
โ + Most visible, easy to cache โ
โ - URL pollution, harder to maintain โ
โ โ
โ 2. Query Parameter Versioning โ
โ GET /api/users?version=1 โ
โ GET /api/users?version=2 โ
โ + Clean URLs, optional version โ
โ - Less visible, caching complexity โ
โ โ
โ 3. Header Versioning โ
โ GET /api/users โ
โ Accept-Version: v1 โ
โ + Clean URLs, flexible โ
โ - Less discoverable, requires documentation โ
โ โ
โ 4. Content Negotiation โ
โ GET /api/users โ
โ Accept: application/vnd.api.v1+json โ
โ + Standard HTTP, clean URLs โ
โ - Complex, less intuitive โ
โ โ
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
Strategy 1: URL Path Versioning
Implementation
// Express.js with URL versioning
import express from 'express';
const app = express();
// v1 routes
app.get('/api/v1/users', (req, res) => {
res.json({
users: [
{ id: 1, name: 'John', email: '[email protected]' }
]
});
});
// v2 routes
app.get('/api/v2/users', (req, res) => {
res.json({
users: [
{
id: 1,
name: 'John',
email: '[email protected]',
profile: { bio: 'Hello', avatar: 'url' }
}
]
});
});
With Router Pattern
// Organize by version
import { Router } from 'express';
const v1Router = Router();
const v2Router = Router();
// v1
v1Router.get('/users', handler.getUsersV1);
v1Router.post('/users', handler.createUserV1);
// v2
v2Router.get('/users', handler.getUsersV2);
v2Router.post('/users', handler.createUserV2);
// Mount
app.use('/api/v1', v1Router);
app.use('/api/v2', v2Router);
Pros and Cons
url_versioning:
pros:
- "Most visible and clear"
- "Easy to test and debug"
- "CDN and cache-friendly"
- "Works with API gateways"
cons:
- "URL pollution over time"
- "Code duplication possible"
- "Requires redirect for default version"
Strategy 2: Query Parameter Versioning
Implementation
// Query parameter versioning
app.get('/api/users', async (req, res) => {
const version = parseInt(req.query.version as string) || 1;
if (version === 1) {
return res.json(await getUsersV1());
}
if (version === 2) {
return res.json(await getUsersV2());
}
res.status(400).json({ error: 'Unsupported version' });
});
// Default version (no query param)
app.get('/api/users', async (req, res) => {
// Default to latest stable version
return res.json(await getUsersV2());
});
Express Router Example
// Route handlers per version
const getUsersHandler = (version: number) => {
return async (req: Request, res: Response) => {
switch (version) {
case 1:
return res.json(await usersService.getUsersV1());
case 2:
return res.json(await usersService.getUsersV2());
default:
return res.status(400).json({ error: 'Invalid version' });
}
};
};
app.get('/api/users', (req, res) => {
const version = parseInt(req.query.v as string) || getDefaultVersion();
getUsersHandler(version)(req, res);
});
Strategy 3: Header Versioning
Implementation
// Header-based versioning
app.get('/api/users', async (req, res) => {
const version = req.headers['x-api-version'] as string || 'v1';
switch (version) {
case 'v1':
return res.json(await getUsersV1());
case 'v2':
return res.json(await getUsersV2());
default:
return res.status(400).json({ error: 'Unsupported version' });
}
});
Custom Header Middleware
// Middleware for header versioning
const versionMiddleware = (req: Request, res: Response, next: NextFunction) => {
const version = req.headers['x-api-version'] as string || 'v1';
req.apiVersion = version;
next();
};
app.use(versionMiddleware);
// Use in handlers
app.get('/api/users', async (req, res) => {
const { apiVersion } = req as Request & { apiVersion: string };
// Handle different versions
});
Strategy 4: Content Negotiation
Implementation
// Content negotiation versioning
app.get('/api/users', async (req, res) => {
const accept = req.headers.accept;
// Parse custom media type
const match = accept?.match(/application\/vnd\.api\.v(\d+)\+json/);
const version = match ? parseInt(match[1]) : 1;
if (version === 1) {
res.set('Content-Type', 'application/vnd.api.v1+json');
return res.json(await getUsersV1());
}
});
Register Custom Media Types
// Register in Express
app.use((req, res, next) => {
// Add custom formatter
res.format({
'application/vnd.api.v1+json': () => {
res.json(getUsersV1());
},
'application/vnd.api.v2+json': () => {
res.json(getUsersV2());
},
'application/json': () => {
// Default
res.json(getUsersV2());
},
default: () => {
res.status(406).json({ error: 'Not acceptable' });
}
});
next();
});
Deprecation Strategy
Marking Endpoints as Deprecated
// Add deprecation headers
app.get('/api/v1/users', (req, res) => {
// Deprecation header (RFC 5988)
res.set('Deprecation', 'Thu, 01 Jan 2026 00:00:00 GMT');
// Link to new version
res.set('Link', '<https://api.example.com/v2/users>; rel="alternate"');
// Sunset header (draft)
res.set('Sunset', 'Thu, 01 Jan 2027 00:00:00 GMT');
// Warning header
res.set('Warning', '299 - "This endpoint will be removed on January 1, 2027"');
res.json(getUsersV1());
});
Deprecation Schedule
# Version lifecycle
lifecycle:
experimental:
- "Newly released"
- "May change without notice"
- "Not recommended for production"
stable:
- "Production-ready"
- "Fully supported"
- "Security updates"
deprecated:
- "Marked for removal"
- "6-12 months of support"
- "New code should use newer version"
removed:
- "No longer available"
- "Returns 410 Gone"
Best Practices
Version Numbering
# Semantic versioning for APIs
versioning:
major:
- "Breaking changes"
- "Remove fields"
- "Change field types"
- "Remove endpoints"
minor:
- "New features (backward compatible)"
- "Add new fields"
- "Add new endpoints"
patch:
- "Bug fixes"
- "Documentation updates"
Default Version Strategy
// Always redirect to latest version
app.get('/api/users', (req, res) => {
// Redirect to v2
res.redirect(307, '/api/v2/users');
});
// Or use default version header
app.get('/api/users', (req, res) => {
const version = req.headers['x-api-version'] || 'v2';
// Handle version
});
Key Takeaways
- URL Path - Most common, best visibility, easiest to cache
- Query Parameter - Clean URLs, optional versioning
- Header - Flexible, less cluttered URLs
- Content Negotiation - Standard HTTP, more complex setup
External Resources
- RFC 5988 - Web Linking
- [API Versioning Best Practices](https://blog.apisyouwont hate.com/2016/12/09/an-argument-for-versioning-your-api/)
- Microsoft API Versioning
Comments