API Versioning
API versioning enables backward compatibility while allowing improvements. Learn strategies for versioning RESTful and GraphQL APIs.
Challenge
Section titled “Challenge”Implement API versioning that maintains backward compatibility, enables feature development, and provides smooth migration paths for clients.
Explanation
Section titled “Explanation”Versioning Strategies
Section titled “Versioning Strategies”URL Path: /api/v1/users - Most common
Query Parameter: /api/users?version=1 - Simpler but less RESTful
Header: API-Version: 1 - Clean but less discoverable
Subdomain: v1.api.example.com - Clean but complex routing
Content Negotiation: Accept: application/vnd.company.v1+json - Most RESTful
Versioning Principles
Section titled “Versioning Principles”- Backward Compatibility: Maintain old versions
- Clear Deprecation: Announce version EOL
- Migration Path: Help clients upgrade
- Documentation: Clear changelog and guides
Code Examples
Section titled “Code Examples”1. URL-Based Versioning
Section titled “1. URL-Based Versioning”const express = require('express');const app = express();
// API routes separated by versionapp.use('/api/v1', require('./routes/v1'));app.use('/api/v2', require('./routes/v2'));app.use('/api/v3', require('./routes/v3'));
// Redirect old versions with sunset warningapp.use('/api/v1/*', (req, res) => { res.setHeader('Sunset', new Date(Date.now() + 90 * 24 * 60 * 60 * 1000).toUTCString()); res.setHeader('Deprecated', 'true'); // Forward to v1 routes...});
// routes/v1/users.jsconst express = require('express');const router = express.Router();
router.get('/:id', (req, res) => { res.json({ id: req.params.id, name: 'John Doe', email: 'john@example.com', });});
module.exports = router;
// routes/v2/users.jsconst express = require('express');const router = express.Router();
router.get('/:id', (req, res) => { res.json({ id: req.params.id, firstName: 'John', lastName: 'Doe', email: 'john@example.com', createdAt: new Date(), });});
module.exports = router;2. Header-Based Versioning
Section titled “2. Header-Based Versioning”const express = require('express');const app = express();
// Middleware to extract versionapp.use((req, res, next) => { // API-Version header or accept version const version = req.headers['api-version'] || req.query.version || 'v1'; // default
req.apiVersion = version; next();});
// Route handler for multiple versionsapp.get('/users/:id', (req, res) => { const { apiVersion } = req;
if (apiVersion === 'v1') { return res.json({ id: req.params.id, name: 'John Doe', email: 'john@example.com', }); }
if (apiVersion === 'v2') { return res.json({ id: req.params.id, firstName: 'John', lastName: 'Doe', email: 'john@example.com', createdAt: new Date(), updatedAt: new Date(), }); }
if (apiVersion === 'v3') { return res.json({ data: { id: req.params.id, firstName: 'John', lastName: 'Doe', email: 'john@example.com', }, meta: { createdAt: new Date(), updatedAt: new Date(), }, }); }
res.status(400).json({ error: 'Unsupported API version' });});3. Version-Aware Middleware
Section titled “3. Version-Aware Middleware”class VersionedAPI { constructor() { this.handlers = new Map(); this.deprecations = new Map(); }
registerHandler(path, method, version, handler) { const key = `${method}:${path}`; if (!this.handlers.has(key)) { this.handlers.set(key, new Map()); } this.handlers.get(key).set(version, handler); }
registerDeprecation(version, message, sunset) { this.deprecations.set(version, { message, sunset }); }
getHandler(path, method, version) { const key = `${method}:${path}`; const versionHandlers = this.handlers.get(key);
if (!versionHandlers) { return null; }
// Try exact version match if (versionHandlers.has(version)) { return versionHandlers.get(version); }
// Try latest compatible version const versions = Array.from(versionHandlers.keys()) .filter(v => v <= version) .sort((a, b) => b - a);
return versions.length > 0 ? versionHandlers.get(versions[0]) : null; }
middleware() { return (req, res, next) => { const version = parseInt(req.headers['api-version'] || '1'); const handler = this.getHandler(req.path, req.method, version);
if (!handler) { return res.status(404).json({ error: 'Endpoint not found' }); }
// Add deprecation headers if (this.deprecations.has(version)) { const { message, sunset } = this.deprecations.get(version); res.setHeader('Deprecated', 'true'); res.setHeader('Deprecation', 'true'); res.setHeader('Sunset', sunset.toUTCString()); res.setHeader('Warning', `299 - "${message}"`); }
handler(req, res, next); }; }}
// Usageconst api = new VersionedAPI();
api.registerHandler('/users/:id', 'GET', 1, (req, res) => { res.json({ id: req.params.id, name: 'John Doe', });});
api.registerHandler('/users/:id', 'GET', 2, (req, res) => { res.json({ id: req.params.id, firstName: 'John', lastName: 'Doe', createdAt: new Date(), });});
api.registerDeprecation(1, 'Version 1 is deprecated, upgrade to v2', new Date('2024-12-31'));
app.use(api.middleware());4. Semantic Versioning for APIs
Section titled “4. Semantic Versioning for APIs”class APIVersion { constructor(version) { const [major, minor, patch] = version.split('.').map(Number); this.major = major; this.minor = minor; this.patch = patch; }
isCompatible(requiredVersion) { const required = new APIVersion(requiredVersion);
// Major version must match if (this.major !== required.major) { return false; }
// Minor version must be >= required if (this.minor < required.minor) { return false; }
// If minor versions match, patch must be >= if (this.minor === required.minor && this.patch < required.patch) { return false; }
return true; }
toString() { return `${this.major}.${this.minor}.${this.patch}`; }}
// Feature compatibility checkingconst FEATURE_VERSIONS = { 'user-profile': '2.0.0', 'batch-operations': '2.1.0', 'webhooks': '3.0.0',};
function requiresFeature(feature, clientVersion) { const required = FEATURE_VERSIONS[feature]; const version = new APIVersion(clientVersion);
return version.isCompatible(required);}
// Usageconst clientVersion = '2.1.5';if (requiresFeature('webhooks', clientVersion)) { // Enable webhooks} else { res.status(402).json({ error: 'Feature requires API version 3.0.0 or higher' });}5. GraphQL Versioning
Section titled “5. GraphQL Versioning”const { buildSchema, graphql } = require('graphql');
// Schema evolution with directivesconst schema = buildSchema(` directive @deprecated(reason: String) on FIELD_DEFINITION
type User { id: ID! name: String! email: String! # Deprecated, use firstName and lastName instead fullName: String @deprecated(reason: "Use firstName and lastName") firstName: String lastName: String }
type Query { user(id: ID!): User users: [User!]! # Deprecated, use allUsers listUsers: [User!]! @deprecated(reason: "Use users instead") }`);
// Resolver implementationconst resolvers = { Query: { user: (args) => fetchUser(args.id), users: () => fetchAllUsers(), listUsers: () => fetchAllUsers(), // Backward compat }, User: { fullName: (user) => `${user.firstName} ${user.lastName}`, },};
// Check for deprecated fields in queryfunction validateQuery(query) { const deprecatedPattern = /@deprecated/; const introspectionQuery = ` query { __type(name: "Query") { fields { name isDeprecated deprecationReason } } } `;
return graphql(schema, introspectionQuery);}6. Version Migration Guide
Section titled “6. Version Migration Guide”// Migration helper for clientsclass APIClient { constructor(baseURL, version = '3') { this.baseURL = baseURL; this.version = version; }
async get(endpoint) { const response = await fetch(`${this.baseURL}/api/v${this.version}${endpoint}`, { headers: { 'API-Version': this.version, }, });
if (response.headers.has('Deprecation')) { console.warn('API version deprecated:', response.headers.get('Warning')); console.warn('Sunset:', response.headers.get('Sunset')); }
return response.json(); }
async migrateFromV2() { // Help users migrate from v2 to v3 console.log('Migrating from API v2 to v3');
// Example: v2 returns 'name', v3 returns 'firstName' and 'lastName' const oldData = await this.fetchV2Data(); return { firstName: oldData.name.split(' ')[0], lastName: oldData.name.split(' ')[1], }; }}Best Practices
Section titled “Best Practices”-
Version Planning
- Plan major versions carefully
- Provide clear upgrade paths
- Maintain compatibility windows
-
Communication
- Use HTTP headers for version info
- Document all changes
- Provide migration guides
- Announce deprecations early
-
Support Timeline
- Define support periods
- Provide sunset dates
- Monitor version usage
- Gradually retire old versions
-
Testing
- Test version compatibility
- Test migration paths
- Verify backward compatibility
- Test deprecated features
-
Monitoring
- Track version usage
- Monitor deprecation adoption
- Alert on version mismatches
- Log version in requests
Real-World Applications
Section titled “Real-World Applications”- Public APIs: Multiple version support
- Mobile Apps: Manage app version compatibility
- SaaS Platforms: Gradual feature rollout
- Microservices: Independent service versioning
- Third-party Integrations: Smooth API updates