Files
pos-system/.agent/rules/api-versioning-strategy.md

11 KiB

trigger
trigger
always_on

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

URL Path Versioning

Implementation

// src/routes/index.ts
// EN: Route versioning
// VI: Route versioning
import { Router } from 'express';
import v1Router from './v1';
import v2Router from './v2';

const router = Router();

// EN: Version 1 routes
// VI: Routes version 1
router.use('/v1', v1Router);

// EN: Version 2 routes
// VI: Routes version 2
router.use('/v2', v2Router);

export default router;

Version Router

// src/routes/v1/index.ts
// EN: Version 1 routes
// VI: Routes version 1
import { Router } from 'express';
import userRoutes from './users';

const router = Router();

router.use('/users', userRoutes);

export default router;

// src/routes/v2/index.ts
// EN: Version 2 routes with breaking changes
// VI: Routes version 2 với breaking changes
import { Router } from 'express';
import userRoutes from './users'; // EN: Different implementation / VI: Implementation khác

const router = Router();

router.use('/users', userRoutes); // EN: Different response format / VI: Format response khác

export default router;

Header-Based Versioning

Version Negotiation Middleware

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

export function versionNegotiation(
  req: Request,
  res: Response,
  next: NextFunction
): void {
  // EN: Extract version from Accept header
  // VI: Trích xuất version từ Accept header
  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;

    // EN: Check if version is supported
    // VI: Kiểm tra xem version có được hỗ trợ không
    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 {
    // EN: Default to latest version
    // VI: Mặc định version mới nhất
    req.apiVersion = 2;
  }

  next();
}

Version-Aware Controller

// src/modules/user/user.controller.ts
// EN: Version-aware controller
// VI: Controller nhận biết version
export class UserController {
  async getUser(req: Request, res: Response): Promise<void> {
    const version = req.apiVersion || 2;

    if (version === 1) {
      // EN: Version 1 response format
      // VI: Format response version 1
      const user = await this.userService.findById(req.params.id);
      res.json({
        id: user.id,
        email: user.email,
        name: user.name,
      });
    } else {
      // EN: Version 2 response format
      // VI: Format response version 2
      const user = await this.userService.findById(req.params.id);
      res.json({
        success: true,
        data: {
          user: {
            id: user.id,
            email: user.email,
            name: user.name,
            profile: user.profile, // EN: New field in v2 / VI: Field mới trong v2
          },
        },
        metadata: {
          version: '2.0.0',
          timestamp: new Date().toISOString(),
        },
      });
    }
  }
}

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
// EN: Add version to response
// VI: Thêm version vào response
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();
}

API Deprecation

Deprecation Headers

// src/middlewares/deprecation.middleware.ts
// EN: Deprecation warning middleware
// VI: Middleware cảnh báo deprecation
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();
  };
}

// Usage
router.use('/v1', deprecationMiddleware('2', '2024-12-31'), v1Router);

Deprecation Documentation

// src/docs/deprecation.md
// EN: Deprecation notices
// VI: Thông báo deprecation

## API Version 1 Deprecation

**Deprecated**: 2024-01-01  
**Sunset Date**: 2024-12-31  
**Migration Guide**: [Migration Guide](./migration-v1-to-v2.md)

### Changes in Version 2

- New response format
- Additional user profile fields
- Updated authentication flow

Backward Compatibility

Compatibility Layer

// src/core/api/compatibility.adapter.ts
// EN: Backward compatibility adapter
// VI: Adapter tương thích ngược
export class CompatibilityAdapter {
  /**
   * EN: Adapt v1 response to v2 format
   * VI: Adapt response v1 sang format v2
   */
  adaptV1ToV2(v1Data: any): any {
    return {
      success: true,
      data: {
        user: {
          ...v1Data,
          profile: null, // EN: Add default for new field / VI: Thêm mặc định cho field mới
        },
      },
      metadata: {
        version: '2.0.0',
        adapted: true,
      },
    };
  }

  /**
   * EN: Adapt v2 request to v1 format
   * VI: Adapt request v2 sang format v1
   */
  adaptV2RequestToV1(v2Request: any): any {
    return {
      email: v2Request.email,
      name: v2Request.name,
      // EN: Ignore new fields / VI: Bỏ qua các field mới
    };
  }
}

Breaking Changes Migration

Migration Strategy

// src/core/api/migration.strategy.ts
// EN: Migration strategy for breaking changes
// VI: Chiến lược migration cho breaking changes
export class MigrationStrategy {
  /**
   * EN: Phase 1: Support both versions
   * VI: Giai đoạn 1: Hỗ trợ cả hai versions
   */
  phase1SupportBoth(): void {
    // EN: Keep v1, add v2
    // VI: Giữ v1, thêm v2
    router.use('/v1', v1Router);
    router.use('/v2', v2Router);
  }

  /**
   * EN: Phase 2: Deprecate v1
   * VI: Giai đoạn 2: Deprecate v1
   */
  phase2DeprecateV1(): void {
    router.use('/v1', deprecationMiddleware('2', '2024-12-31'), v1Router);
    router.use('/v2', v2Router);
  }

  /**
   * EN: Phase 3: Remove v1
   * VI: Giai đoạn 3: Xóa v1
   */
  phase3RemoveV1(): void {
    // EN: After sunset date, remove v1 routes
    // VI: Sau sunset date, xóa v1 routes
    router.use('/v2', v2Router);
  }
}

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
    
  4. No Migration Guide: Clients don't know how to upgrade

    # ✅ Always provide:
    - Changelog
    - Migration guide
    - Sunset date
    - Deprecation warnings
    

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"

Version Detection:

// URL path
const version = req.path.match(/\/v(\d+)\//)?.[1] || '2';

// Header
const version = req.headers.accept?.match(/vnd\.goodgo\.v(\d+)/)?.[1];