24 KiB
description, shortcut
| description | shortcut |
|---|---|
| Manage API versions with proper migration strategies | 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:
- Define versioning strategy and policy
- Identify breaking vs. non-breaking changes
- Plan deprecation timeline (typically 6-12 months)
- Set up monitoring for version usage
- 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 implementationapi/v2/- Version 2 implementationmiddleware/version-router.js- Version routing logictransformers/- Version-specific data transformerstests/compatibility/- Cross-version compatibility testsdocs/migration-guide.md- Version migration documentation
Code Examples
Example 1: Comprehensive URL Path Versioning System
// 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': `</docs/migration/${requestedVersion}>; 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
// 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
// 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 headerquery: Version in query parametersubdomain: Version in subdomain (v1.api.example.com)
Deprecation Policies
aggressive: 3-month deprecation cyclestandard: 6-month deprecation cycleconservative: 12-month deprecation cycleenterprise: 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
// 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