903 lines
24 KiB
Markdown
903 lines
24 KiB
Markdown
---
|
|
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': `</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
|
|
|
|
```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 |