Files
gh-jeremylongshore-claude-c…/commands/manage-api-versions.md
2025-11-29 18:52:42 +08:00

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:

  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

// 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 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

// 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();
});
  • /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