Files
pos-system/docs/en/skills/api-versioning-strategy.md
Ho Ngoc Hai 2640b351c3 Enhance documentation with detailed diagrams and structured flows
- Added request/response flow diagrams to api-design and api-gateway-advanced skills for better visualization of processes.
- Introduced configuration loading flow in configuration-management skill to clarify the configuration process.
- Included error propagation flow in error-handling-patterns skill to illustrate error handling across layers.
- Enhanced various skills with additional diagrams to improve understanding of complex concepts.

These updates aim to provide clearer guidance and improve the overall documentation experience for developers.
2026-01-01 23:22:54 +07:00

12 KiB

name, description
name description
api-versioning-strategy API versioning strategies for GoodGo microservices including semantic versioning, backward compatibility patterns, API deprecation, version negotiation, and breaking changes handling.

API Versioning Strategy

When to Use This Skill

Use this skill when:

  • Versioning APIs
  • Handling breaking changes
  • Implementing API deprecation
  • Maintaining backward compatibility
  • Implementing version negotiation
  • Managing multiple API versions
  • Planning API evolution
  • Communicating API changes to consumers

Core Concepts

Versioning Strategies

  1. URL Path Versioning: /api/v1/users, /api/v2/users
  2. Header Versioning: Accept: application/vnd.goodgo.v1+json
  3. Query Parameter: /api/users?version=1
  4. Semantic Versioning: Major.Minor.Patch (e.g., 1.2.3)

Compatibility Types

  • Backward Compatible: New version works with old clients
  • Forward Compatible: Old version works with new clients
  • Breaking Changes: Incompatible changes requiring new version

Version Negotiation

Version negotiation allows clients to request a specific API version through headers while maintaining clean URLs. The middleware extracts the version from the Accept header and routes to the appropriate handler.

sequenceDiagram
    participant Client
    participant Middleware as Version Negotiation<br/>Middleware
    participant Controller as Version-Aware<br/>Controller
    participant Service

    Client->>Middleware: Request with Accept header<br/>Accept: application/vnd.goodgo.v1+json
    Middleware->>Middleware: Extract version from header
    alt Version specified
        Middleware->>Middleware: Parse version number
        alt Version supported
            Middleware->>Controller: Set req.apiVersion = 1
            Controller->>Controller: Check version
            Controller->>Service: Call service method
            Service-->>Controller: Return data
            Controller->>Controller: Format response for v1
            Controller-->>Client: v1 response format
        else Version not supported
            Middleware-->>Client: 400 Unsupported Version
        end
    else No version specified
        Middleware->>Controller: Set req.apiVersion = latest (2)
        Controller->>Service: Call service method
        Service-->>Controller: Return data
        Controller->>Controller: Format response for v2
        Controller-->>Client: v2 response format (default)
    end

Implementation

// src/middlewares/version-negotiation.middleware.ts
import { Request, Response, NextFunction } from 'express';
import { logger } from '@goodgo/logger';

export function versionNegotiation(
  req: Request,
  res: Response,
  next: NextFunction
): void {
  const acceptHeader = req.headers.accept || '';
  const versionMatch = acceptHeader.match(/application\/vnd\.goodgo\.v(\d+)\+json/);

  if (versionMatch) {
    const requestedVersion = parseInt(versionMatch[1], 10);
    req.apiVersion = requestedVersion;

    const supportedVersions = [1, 2];
    if (!supportedVersions.includes(requestedVersion)) {
      return res.status(400).json({
        success: false,
        error: {
          code: 'UNSUPPORTED_VERSION',
          message: `API version ${requestedVersion} is not supported. Supported versions: ${supportedVersions.join(', ')}`,
        },
      });
    }
  } else {
    req.apiVersion = 2; // Default to latest
  }

  next();
}

API Deprecation Timeline

API deprecation follows a structured timeline to give consumers adequate time to migrate. The lifecycle progresses through active, deprecated, sunset, and removed phases.

gantt
    title API Version Lifecycle Timeline
    dateFormat YYYY-MM-DD
    section Version 1
    Active (v1 only)           :active, v1-active, 2024-01-01, 2024-06-01
    Deprecated (v1 + v2)       :crit, v1-deprecated, 2024-06-01, 2024-12-31
    Sunset Period              :v1-sunset, 2024-12-31, 2025-01-31
    Removed                    :v1-removed, 2025-01-31, 1d
    section Version 2
    Development                :v2-dev, 2024-03-01, 2024-06-01
    Active (v1 + v2)           :active, v2-active, 2024-06-01, 2025-12-31

Deprecation Phases

stateDiagram-v2
    [*] --> Active: Version Released
    Active --> Deprecated: New Version Released<br/>Add Deprecation Headers
    Deprecated --> Sunset: Sunset Date Reached<br/>Stop Accepting New Requests
    Sunset --> Removed: Grace Period Ended<br/>Remove Routes
    Removed --> [*]
    
    note right of Active
        - Version fully supported
        - No warnings
        - All features available
    end note
    
    note right of Deprecated
        - Deprecation header set
        - Warning headers added
        - Migration guide provided
        - Still functional
    end note
    
    note right of Sunset
        - Read-only mode
        - No new requests accepted
        - Existing requests honored
    end note

Deprecation Headers

// src/middlewares/deprecation.middleware.ts
export function deprecationMiddleware(version: string, sunsetDate: string) {
  return (req: Request, res: Response, next: NextFunction): void => {
    if (req.apiVersion && parseInt(req.apiVersion.toString()) < parseInt(version)) {
      res.setHeader('Deprecation', 'true');
      res.setHeader('Sunset', sunsetDate);
      res.setHeader('Link', `<${req.url.replace(/\/v\d+/, `/v${version}`)}>; rel="successor-version"`);
      res.setHeader('Warning', `299 - "API version ${req.apiVersion} is deprecated. Please migrate to version ${version} by ${sunsetDate}"`);
    }

    next();
  };
}

Migration Flow

Breaking changes require a careful 3-phase migration strategy to ensure zero downtime and smooth client transitions.

flowchart TD
    Start([Breaking Change Identified]) --> Phase1[Phase 1: Support Both Versions]
    
    Phase1 --> DeployV2[Deploy v2 alongside v1]
    DeployV2 --> Monitor1[Monitor v1 and v2 usage]
    Monitor1 --> Wait1[Wait for client adoption]
    Wait1 --> Phase2{Sufficient<br/>v2 adoption?}
    
    Phase2 -->|No| Wait1
    Phase2 -->|Yes| Phase2Start[Phase 2: Deprecate v1]
    
    Phase2Start --> AddHeaders[Add deprecation headers to v1]
    AddHeaders --> NotifyClients[Notify clients via<br/>deprecation warnings]
    NotifyClients --> ProvideGuide[Provide migration guide]
    ProvideGuide --> Monitor2[Monitor migration progress]
    Monitor2 --> Wait2[Wait until sunset date]
    Wait2 --> Phase3{Sunset date<br/>reached?}
    
    Phase3 -->|No| Monitor2
    Phase3 -->|Yes| Phase3Start[Phase 3: Remove v1]
    
    Phase3Start --> StopAccepting[Stop accepting new v1 requests]
    StopAccepting --> GracePeriod[Grace period for<br/>existing requests]
    GracePeriod --> RemoveRoutes[Remove v1 routes]
    RemoveRoutes --> End([Migration Complete])
    
    style Phase1 fill:#e1f5ff
    style Phase2Start fill:#fff4e1
    style Phase3Start fill:#ffe1e1
    style End fill:#e1ffe1

Implementation Strategy

// src/core/api/migration.strategy.ts
export class MigrationStrategy {
  /**
   * Phase 1: Support both versions
   */
  phase1SupportBoth(): void {
    router.use('/v1', v1Router);
    router.use('/v2', v2Router);
  }

  /**
   * Phase 2: Deprecate v1
   */
  phase2DeprecateV1(): void {
    router.use('/v1', deprecationMiddleware('2', '2024-12-31'), v1Router);
    router.use('/v2', v2Router);
  }

  /**
   * Phase 3: Remove v1
   */
  phase3RemoveV1(): void {
    router.use('/v2', v2Router);
    // v1 routes removed after sunset date
  }
}

URL Path Versioning

Implementation

// src/routes/index.ts
import { Router } from 'express';
import v1Router from './v1';
import v2Router from './v2';

const router = Router();

router.use('/v1', v1Router);
router.use('/v2', v2Router);

export default router;

Semantic Versioning

Version Structure

MAJOR.MINOR.PATCH

MAJOR: Breaking changes
MINOR: Backward-compatible additions
PATCH: Backward-compatible bug fixes

Version Response

// src/core/api/version.middleware.ts
export function versionMiddleware(req: Request, res: Response, next: NextFunction): void {
  const originalJson = res.json.bind(res);

  res.json = (data: any) => {
    const response = {
      ...data,
      metadata: {
        ...data.metadata,
        apiVersion: req.apiVersion || '2.0.0',
        serviceVersion: process.env.SERVICE_VERSION || '1.0.0',
      },
    };

    return originalJson(response);
  };

  next();
}

Backward Compatibility

Compatibility Layer

// src/core/api/compatibility.adapter.ts
export class CompatibilityAdapter {
  adaptV1ToV2(v1Data: any): any {
    return {
      success: true,
      data: {
        user: {
          ...v1Data,
          profile: null, // Add default for new field
        },
      },
      metadata: {
        version: '2.0.0',
        adapted: true,
      },
    };
  }

  adaptV2RequestToV1(v2Request: any): any {
    return {
      email: v2Request.email,
      name: v2Request.name,
      // Ignore new fields
    };
  }
}

Best Practices

  1. Versioning Strategy: Choose URL path or header, be consistent
  2. Semantic Versioning: Use MAJOR.MINOR.PATCH
  3. Deprecation: Always deprecate before removing
  4. Migration Guide: Provide clear migration documentation
  5. Backward Compatibility: Maintain compatibility when possible
  6. Communication: Clearly communicate version changes

Common Mistakes

  1. No Deprecation Period: Breaking clients suddenly

    // ❌ BAD: Remove v1 immediately
    router.use('/v2', v2Router);
    
    // ✅ GOOD: Deprecate with sunset date
    router.use('/v1', deprecationMiddleware('2', '2024-12-31'), v1Router);
    router.use('/v2', v2Router);
    
  2. Breaking Changes Without Major Version: Client confusion

    # ❌ BAD: Breaking change in minor version
    v1.1.0 → Changed response format
    
    # ✅ GOOD: Breaking change = new major version
    v1.x.x → v2.0.0 (new response format)
    
  3. Inconsistent Versioning Strategy: Mixed approaches

    // ❌ BAD: Mix URL and header versioning
    /api/v1/users + Accept: application/vnd.v2+json
    
    // ✅ GOOD: Choose one approach
    /api/v1/users OR Accept: application/vnd.goodgo.v1+json
    

Quick Reference

Strategy Pros Cons Use When
URL Path Clear, cacheable URL changes Public APIs
Header Clean URLs Less visible Internal APIs
Query Param Simple Not RESTful Quick prototypes

Semantic Versioning:

MAJOR.MINOR.PATCH
  │     │     └── Bug fixes (backward compatible)
  │     └──────── New features (backward compatible)
  └────────────── Breaking changes

Version Lifecycle:

v1 Active → v2 Released → v1 Deprecated → v1 Sunset → v1 Removed
   │            │              │              │           │
   │            │         Add headers     Remove from   Delete
   │         Support       + warnings       docs       routes
  Solo        both

Deprecation Headers:

Deprecation: true
Sunset: Sat, 31 Dec 2024 23:59:59 GMT
Warning: 299 - "API v1 is deprecated. Migrate to v2 by 2024-12-31"
Link: </api/v2/users>; rel="successor-version"

Resources