--- description: Manage API versions with proper migration strategies shortcut: apiver --- # Manage API Versions Implement comprehensive API versioning strategies with backward compatibility, smooth migration paths, deprecation workflows, and automated compatibility testing to ensure seamless API evolution. ## When to Use This Command Use `/manage-api-versions` when you need to: - Introduce breaking changes without disrupting existing clients - Support multiple API versions simultaneously - Provide smooth migration paths for API consumers - Implement deprecation strategies with clear timelines - Maintain backward compatibility while innovating - Comply with enterprise SLA requirements DON'T use this when: - Building internal-only APIs with controlled clients (coordinate directly) - API is in early beta with no production users (iterate freely) - Changes are purely additive and backward compatible (versioning unnecessary) ## Design Decisions This command implements **URL Path Versioning + Accept Header** as the primary approach because: - Most intuitive for developers (visible in URL) - Easy to route and cache at infrastructure level - Clear version separation in code organization - Accept headers provide fine-grained control - Works well with API gateways and CDNs - Industry standard for REST APIs **Alternative considered: Header-Only Versioning** - Cleaner URLs - More RESTful approach - Harder to test and debug - Recommended for purist REST APIs **Alternative considered: Query Parameter Versioning** - Easy to implement - Optional versioning support - Can pollute URL structure - Recommended for simple versioning needs ## Prerequisites Before running this command: 1. Define versioning strategy and policy 2. Identify breaking vs. non-breaking changes 3. Plan deprecation timeline (typically 6-12 months) 4. Set up monitoring for version usage 5. Prepare migration documentation ## Implementation Process ### Step 1: Choose Versioning Strategy Select and implement the appropriate versioning mechanism for your API architecture. ### Step 2: Create Version Infrastructure Set up routing, middleware, and transformers for multi-version support. ### Step 3: Implement Compatibility Layer Build backward compatibility adapters and response transformers. ### Step 4: Add Deprecation Workflow Implement deprecation notices, sunset headers, and migration tools. ### Step 5: Set Up Version Testing Create comprehensive test suites covering all supported versions. ## Output Format The command generates: - `api/v1/` - Version 1 implementation - `api/v2/` - Version 2 implementation - `middleware/version-router.js` - Version routing logic - `transformers/` - Version-specific data transformers - `tests/compatibility/` - Cross-version compatibility tests - `docs/migration-guide.md` - Version migration documentation ## Code Examples ### Example 1: Comprehensive URL Path Versioning System ```javascript // middleware/version-router.js const express = require('express'); const semver = require('semver'); class APIVersionManager { constructor(options = {}) { this.versions = new Map(); this.defaultVersion = options.defaultVersion || 'v1'; this.deprecationPolicy = options.deprecationPolicy || { warningPeriod: 90, // days before sunset sunsetPeriod: 180 // days until removal }; this.versionInfo = new Map(); } registerVersion(version, router, metadata = {}) { this.versions.set(version, router); this.versionInfo.set(version, { releaseDate: metadata.releaseDate || new Date(), deprecatedDate: metadata.deprecatedDate, sunsetDate: metadata.sunsetDate, changes: metadata.changes || [], status: metadata.status || 'active' // active, deprecated, sunset }); } createVersionMiddleware() { return (req, res, next) => { // Extract version from URL path const pathMatch = req.path.match(/^\/v(\d+(?:\.\d+)?)/); const version = pathMatch ? `v${pathMatch[1]}` : null; // Check Accept header for version preference const acceptHeader = req.headers.accept || ''; const headerMatch = acceptHeader.match(/application\/vnd\.api\.v(\d+)/); const headerVersion = headerMatch ? `v${headerMatch[1]}` : null; // Determine final version (path takes precedence) const requestedVersion = version || headerVersion || this.defaultVersion; // Validate version exists if (!this.versions.has(requestedVersion)) { return res.status(400).json({ error: 'Invalid API version', message: `Version ${requestedVersion} is not supported`, supportedVersions: Array.from(this.versions.keys()), latestVersion: this.getLatestVersion() }); } // Check if version is deprecated or sunset const versionMeta = this.versionInfo.get(requestedVersion); if (versionMeta.status === 'sunset') { return res.status(410).json({ error: 'API version sunset', message: `Version ${requestedVersion} is no longer available`, sunsetDate: versionMeta.sunsetDate, alternatives: this.getActiveVersions(), migrationGuide: `/docs/migration/${requestedVersion}` }); } // Add version headers res.set({ 'API-Version': requestedVersion, 'X-API-Version': requestedVersion }); // Add deprecation headers if applicable if (versionMeta.status === 'deprecated') { const sunsetDate = versionMeta.sunsetDate || this.calculateSunsetDate(versionMeta.deprecatedDate); res.set({ 'Deprecation': 'true', 'Sunset': sunsetDate.toUTCString(), 'Link': `; rel="deprecation"`, 'Warning': `299 - "Version ${requestedVersion} is deprecated and will be removed on ${sunsetDate.toDateString()}"` }); // Add deprecation notice to response res.on('finish', () => { console.log(`Deprecated API version ${requestedVersion} called by ${req.ip}`); }); } // Store version info in request req.apiVersion = requestedVersion; req.apiVersionInfo = versionMeta; // Route to version-specific handler const versionRouter = this.versions.get(requestedVersion); versionRouter(req, res, next); }; } getLatestVersion() { const versions = Array.from(this.versions.keys()); return versions.sort((a, b) => semver.rcompare(a.slice(1), b.slice(1)))[0]; } getActiveVersions() { return Array.from(this.versionInfo.entries()) .filter(([_, info]) => info.status === 'active') .map(([version, _]) => version); } calculateSunsetDate(deprecatedDate) { const sunset = new Date(deprecatedDate); sunset.setDate(sunset.getDate() + this.deprecationPolicy.sunsetPeriod); return sunset; } generateVersionReport() { const report = { current: this.getLatestVersion(), supported: [], deprecated: [], sunset: [] }; for (const [version, info] of this.versionInfo) { const versionData = { version, releaseDate: info.releaseDate, status: info.status }; switch (info.status) { case 'active': report.supported.push(versionData); break; case 'deprecated': report.deprecated.push({ ...versionData, sunsetDate: info.sunsetDate }); break; case 'sunset': report.sunset.push({ ...versionData, sunsetDate: info.sunsetDate }); break; } } return report; } } // api/v1/routes.js - Version 1 Implementation const v1Router = express.Router(); v1Router.get('/users', async (req, res) => { const users = await getUsersV1(); // V1 response format res.json({ data: users.map(user => ({ id: user.id, name: user.name, email: user.email // V1 doesn't include profile field })) }); }); v1Router.get('/users/:id', async (req, res) => { const user = await getUserByIdV1(req.params.id); res.json({ data: { id: user.id, name: user.name, email: user.email } }); }); // api/v2/routes.js - Version 2 Implementation const v2Router = express.Router(); v2Router.get('/users', async (req, res) => { const users = await getUsersV2(); // V2 response format with additional fields res.json({ data: users.map(user => ({ id: user.id, displayName: user.name, // Field renamed email: user.email, profile: { // New nested object avatar: user.avatar, bio: user.bio, createdAt: user.createdAt } })), meta: { // New metadata section total: users.length, version: 'v2' } }); }); // transformers/v1-to-v2.js - Backward Compatibility Transformer class V1ToV2Transformer { transformUserResponse(v2Response) { // Transform V2 response to V1 format if (Array.isArray(v2Response.data)) { return { data: v2Response.data.map(user => ({ id: user.id, name: user.displayName, // Map back to old field name email: user.email // Omit profile field for V1 })) }; } return { data: { id: v2Response.data.id, name: v2Response.data.displayName, email: v2Response.data.email } }; } transformUserRequest(v1Request) { // Transform V1 request to V2 format return { ...v1Request, displayName: v1Request.name, profile: { // Set defaults for new required fields avatar: null, bio: '', createdAt: new Date().toISOString() } }; } } // Usage const versionManager = new APIVersionManager({ defaultVersion: 'v2', deprecationPolicy: { warningPeriod: 90, sunsetPeriod: 180 } }); // Register versions versionManager.registerVersion('v1', v1Router, { releaseDate: new Date('2023-01-01'), deprecatedDate: new Date('2024-06-01'), status: 'deprecated', changes: ['Initial API release'] }); versionManager.registerVersion('v2', v2Router, { releaseDate: new Date('2024-01-01'), status: 'active', changes: [ 'Renamed user.name to user.displayName', 'Added user.profile nested object', 'Added metadata to list responses' ] }); // Apply versioning middleware app.use('/api', versionManager.createVersionMiddleware()); // Version discovery endpoint app.get('/api/versions', (req, res) => { res.json(versionManager.generateVersionReport()); }); ``` ### Example 2: Advanced Content Negotiation Versioning ```javascript // middleware/content-negotiation.js const accepts = require('accepts'); class ContentNegotiationVersioning { constructor() { this.handlers = new Map(); this.transformers = new Map(); } register(version, mediaType, handler, transformer = null) { const key = `${version}:${mediaType}`; this.handlers.set(key, handler); if (transformer) { this.transformers.set(key, transformer); } } negotiate() { return async (req, res, next) => { const accept = accepts(req); // Define supported media types with versions const supportedTypes = [ 'application/vnd.api.v3+json', 'application/vnd.api.v2+json', 'application/vnd.api.v1+json', 'application/json' // Default fallback ]; const acceptedType = accept.type(supportedTypes); if (!acceptedType) { return res.status(406).json({ error: 'Not Acceptable', message: 'None of the requested media types are supported', supported: supportedTypes }); } // Extract version from media type let version = 'v2'; // Default version let format = 'json'; const versionMatch = acceptedType.match(/v(\d+)/); if (versionMatch) { version = `v${versionMatch[1]}`; } const formatMatch = acceptedType.match(/\+(\w+)$/); if (formatMatch) { format = formatMatch[1]; } // Set request properties req.apiVersion = version; req.responseFormat = format; // Override res.json to apply version transformations const originalJson = res.json.bind(res); res.json = function(data) { // Apply version-specific transformations const transformerKey = `${version}:${format}`; const transformer = this.transformers.get(transformerKey); if (transformer) { data = transformer(data, req); } // Set content type header res.type(acceptedType); // Add version headers res.set({ 'Content-Type': acceptedType, 'API-Version': version, 'Vary': 'Accept' // Important for caching }); return originalJson(data); }.bind(this); next(); }; } } // services/version-compatibility.js class VersionCompatibilityService { constructor() { this.breakingChanges = new Map(); this.migrationStrategies = new Map(); } registerBreakingChange(fromVersion, toVersion, change) { const key = `${fromVersion}->${toVersion}`; if (!this.breakingChanges.has(key)) { this.breakingChanges.set(key, []); } this.breakingChanges.get(key).push(change); } registerMigrationStrategy(fromVersion, toVersion, strategy) { const key = `${fromVersion}->${toVersion}`; this.migrationStrategies.set(key, strategy); } analyzeCompatibility(fromVersion, toVersion, data) { const key = `${fromVersion}->${toVersion}`; const changes = this.breakingChanges.get(key) || []; const issues = []; for (const change of changes) { if (change.detector(data)) { issues.push({ type: change.type, field: change.field, description: change.description, severity: change.severity, migration: change.migration }); } } return { compatible: issues.length === 0, issues, canAutoMigrate: issues.every(i => i.migration !== null) }; } migrate(fromVersion, toVersion, data) { const key = `${fromVersion}->${toVersion}`; const strategy = this.migrationStrategies.get(key); if (!strategy) { throw new Error(`No migration strategy from ${fromVersion} to ${toVersion}`); } return strategy(data); } } // Example breaking changes registration const compatibilityService = new VersionCompatibilityService(); compatibilityService.registerBreakingChange('v1', 'v2', { type: 'field_renamed', field: 'name', description: 'Field "name" renamed to "displayName"', severity: 'major', detector: (data) => data.hasOwnProperty('name'), migration: (data) => { data.displayName = data.name; delete data.name; return data; } }); compatibilityService.registerBreakingChange('v1', 'v2', { type: 'field_added_required', field: 'profile', description: 'Required field "profile" added', severity: 'major', detector: (data) => !data.hasOwnProperty('profile'), migration: (data) => { data.profile = { avatar: null, bio: '', createdAt: new Date().toISOString() }; return data; } }); // Migration strategy for v1 to v2 compatibilityService.registerMigrationStrategy('v1', 'v2', (data) => { // Full migration logic const migrated = { ...data }; // Rename fields if (migrated.name) { migrated.displayName = migrated.name; delete migrated.name; } // Add new required fields if (!migrated.profile) { migrated.profile = { avatar: null, bio: '', createdAt: new Date().toISOString() }; } // Transform nested structures if (migrated.addresses && Array.isArray(migrated.addresses)) { migrated.locations = migrated.addresses.map(addr => ({ type: addr.type || 'home', address: addr, isPrimary: addr.primary || false })); delete migrated.addresses; } return migrated; }); ``` ### Example 3: Automated Version Testing and Documentation ```javascript // tests/version-compatibility.test.js const request = require('supertest'); const app = require('../app'); class VersionCompatibilityTester { constructor(app) { this.app = app; this.versions = ['v1', 'v2', 'v3']; this.endpoints = []; this.results = []; } addEndpoint(method, path, testCases) { this.endpoints.push({ method, path, testCases }); } async runCompatibilityTests() { console.log('Running API version compatibility tests...\n'); for (const endpoint of this.endpoints) { for (const version of this.versions) { for (const testCase of endpoint.testCases) { const result = await this.testEndpoint( version, endpoint.method, endpoint.path, testCase ); this.results.push(result); // Log result const status = result.passed ? '✓' : '✗'; console.log( `${status} ${version} ${endpoint.method} ${endpoint.path} - ${testCase.name}` ); } } } return this.generateReport(); } async testEndpoint(version, method, path, testCase) { const url = `/api/${version}${path}`; try { const response = await request(this.app) [method.toLowerCase()](url) .send(testCase.payload || {}) .set('Accept', `application/vnd.api.${version}+json`) .expect(testCase.expectedStatus || 200); // Validate response structure const validation = this.validateResponse( version, response.body, testCase.expectedSchema ); return { version, endpoint: `${method} ${path}`, testCase: testCase.name, passed: validation.valid, errors: validation.errors, response: response.body }; } catch (error) { return { version, endpoint: `${method} ${path}`, testCase: testCase.name, passed: false, errors: [error.message], response: null }; } } validateResponse(version, response, expectedSchema) { // Version-specific validation logic const errors = []; // Check required fields for (const field of expectedSchema.required || []) { if (!response.hasOwnProperty(field)) { errors.push(`Missing required field: ${field}`); } } // Check field types for (const [field, type] of Object.entries(expectedSchema.properties || {})) { if (response[field] !== undefined && typeof response[field] !== type) { errors.push(`Invalid type for ${field}: expected ${type}`); } } return { valid: errors.length === 0, errors }; } generateReport() { const report = { timestamp: new Date().toISOString(), summary: { total: this.results.length, passed: this.results.filter(r => r.passed).length, failed: this.results.filter(r => !r.passed).length }, versionMatrix: this.generateVersionMatrix(), failures: this.results.filter(r => !r.passed), recommendations: this.generateRecommendations() }; // Save report require('fs').writeFileSync( 'version-compatibility-report.json', JSON.stringify(report, null, 2) ); return report; } generateVersionMatrix() { const matrix = {}; for (const version of this.versions) { matrix[version] = { endpoints: {}, compatibility: 0 }; for (const endpoint of this.endpoints) { const key = `${endpoint.method} ${endpoint.path}`; const results = this.results.filter( r => r.version === version && r.endpoint === key ); matrix[version].endpoints[key] = { total: results.length, passed: results.filter(r => r.passed).length }; } // Calculate compatibility percentage const versionResults = this.results.filter(r => r.version === version); matrix[version].compatibility = ( (versionResults.filter(r => r.passed).length / versionResults.length) * 100 ).toFixed(2); } return matrix; } generateRecommendations() { const recommendations = []; // Check for consistent failures across versions const failurePatterns = {}; for (const failure of this.results.filter(r => !r.passed)) { const key = failure.endpoint; if (!failurePatterns[key]) { failurePatterns[key] = new Set(); } failurePatterns[key].add(failure.version); } for (const [endpoint, versions] of Object.entries(failurePatterns)) { if (versions.size > 1) { recommendations.push({ type: 'cross_version_failure', endpoint, affectedVersions: Array.from(versions), recommendation: 'Review endpoint implementation for version-agnostic issues' }); } } return recommendations; } } // Usage const tester = new VersionCompatibilityTester(app); // Add test cases tester.addEndpoint('GET', '/users', [ { name: 'List users', expectedStatus: 200, expectedSchema: { required: ['data'], properties: { data: 'object' } } } ]); tester.addEndpoint('POST', '/users', [ { name: 'Create user with v1 format', payload: { name: 'John Doe', email: 'john@example.com' }, expectedStatus: 201 }, { name: 'Create user with v2 format', payload: { displayName: 'John Doe', email: 'john@example.com', profile: { bio: 'Developer' } }, expectedStatus: 201 } ]); // Run tests tester.runCompatibilityTests() .then(report => { console.log('\nCompatibility Report Generated'); console.log(`Total Tests: ${report.summary.total}`); console.log(`Passed: ${report.summary.passed}`); console.log(`Failed: ${report.summary.failed}`); }); ``` ## Error Handling | Error | Cause | Solution | |-------|-------|----------| | "Invalid API version" | Unsupported version requested | Return list of supported versions | | "Version sunset" | Version no longer available | Provide migration guide and alternatives | | "Incompatible request" | Breaking changes detected | Apply automatic migration if possible | | "Deprecation warning ignored" | Client using deprecated version | Send stronger warnings, contact client | | "Version routing conflict" | Overlapping route definitions | Review route precedence rules | ## Configuration Options **Versioning Strategies** - `url-path`: Version in URL path (/v1/) - `header`: Version in Accept header - `query`: Version in query parameter - `subdomain`: Version in subdomain (v1.api.example.com) **Deprecation Policies** - `aggressive`: 3-month deprecation cycle - `standard`: 6-month deprecation cycle - `conservative`: 12-month deprecation cycle - `enterprise`: Custom per-client agreements ## Best Practices DO: - Support at least 2 major versions simultaneously - Provide clear deprecation timelines - Version your database schemas - Maintain comprehensive migration documentation - Use semantic versioning - Monitor version usage analytics DON'T: - Remove versions without notice - Make breaking changes in minor versions - Ignore backward compatibility - Version too granularly - Mix versioning strategies ## Performance Considerations - Cache responses per version - Lazy-load version-specific code - Use CDN with version-aware caching - Monitor performance per version - Optimize hot migration paths ## Monitoring and Analytics ```javascript // Track version usage const versionMetrics = { requests: new Map(), deprecated: new Map(), errors: new Map() }; app.use((req, res, next) => { const version = req.apiVersion || 'unknown'; versionMetrics.requests.set( version, (versionMetrics.requests.get(version) || 0) + 1 ); next(); }); ``` ## Related Commands - `/api-documentation-generator` - Generate version-specific docs - `/api-sdk-generator` - Create versioned SDKs - `/api-testing-framework` - Test version compatibility - `/api-migration-tool` - Automate version migrations ## Version History - v1.0.0 (2024-10): Initial implementation with URL path versioning - Planned v1.1.0: Add GraphQL schema versioning support