api-versioning-strategy

Compare original and translation side by side

🇺🇸

Original

English
🇨🇳

Translation

Chinese

API Versioning Strategy

API版本控制策略

Overview

概述

Comprehensive guide to API versioning approaches, deprecation strategies, backward compatibility techniques, and migration planning for REST APIs, GraphQL, and gRPC services.
这是一份针对REST APIs、GraphQL和gRPC服务的API版本控制方法、弃用策略、向后兼容技术以及迁移规划的综合指南。

When to Use

适用场景

  • Designing new APIs with versioning from the start
  • Adding breaking changes to existing APIs
  • Deprecating old API versions
  • Planning API migrations
  • Ensuring backward compatibility
  • Managing multiple API versions simultaneously
  • Creating API documentation for different versions
  • Implementing API version routing
  • 从设计初期就为新API加入版本控制
  • 为现有API添加破坏性变更
  • 弃用旧版API
  • 规划API迁移
  • 确保向后兼容性
  • 同时管理多个API版本
  • 为不同版本创建API文档
  • 实现API版本路由

Instructions

操作指南

1. Versioning Approaches

1. 版本控制方法

URL Path Versioning

URL路径版本控制

typescript
// express-router.ts
import express from 'express';

const app = express();

// Version 1
app.get('/api/v1/users', (req, res) => {
  res.json({
    users: [
      { id: 1, name: 'John Doe' }
    ]
  });
});

// Version 2 - Added email field
app.get('/api/v2/users', (req, res) => {
  res.json({
    users: [
      { id: 1, name: 'John Doe', email: 'john@example.com' }
    ]
  });
});

// Shared logic with version-specific transformations
app.get('/api/:version/users/:id', async (req, res) => {
  const user = await userService.findById(req.params.id);

  if (req.params.version === 'v1') {
    res.json({ id: user.id, name: user.name });
  } else if (req.params.version === 'v2') {
    res.json({ id: user.id, name: user.name, email: user.email });
  }
});
Pros: Simple, explicit, cache-friendly Cons: URL pollution, harder to deprecate
typescript
// express-router.ts
import express from 'express';

const app = express();

// Version 1
app.get('/api/v1/users', (req, res) => {
  res.json({
    users: [
      { id: 1, name: 'John Doe' }
    ]
  });
});

// Version 2 - Added email field
app.get('/api/v2/users', (req, res) => {
  res.json({
    users: [
      { id: 1, name: 'John Doe', email: 'john@example.com' }
    ]
  });
});

// Shared logic with version-specific transformations
app.get('/api/:version/users/:id', async (req, res) => {
  const user = await userService.findById(req.params.id);

  if (req.params.version === 'v1') {
    res.json({ id: user.id, name: user.name });
  } else if (req.params.version === 'v2') {
    res.json({ id: user.id, name: user.name, email: user.email });
  }
});
优点: 简单直观、支持缓存 缺点: 污染URL、弃用难度大

Header Versioning (Content Negotiation)

请求头版本控制(内容协商)

typescript
// header-versioning.ts
app.get('/api/users', (req, res) => {
  const version = req.headers['api-version'] || '1';

  switch (version) {
    case '1':
      return res.json(transformToV1(users));
    case '2':
      return res.json(transformToV2(users));
    default:
      return res.status(400).json({ error: 'Unsupported API version' });
  }
});

// Or using Accept header
app.get('/api/users', (req, res) => {
  const acceptHeader = req.headers['accept'];

  if (acceptHeader.includes('application/vnd.myapi.v2+json')) {
    return res.json(transformToV2(users));
  }

  // Default to v1
  return res.json(transformToV1(users));
});
Pros: Clean URLs, RESTful Cons: Less visible, harder to test manually
typescript
// header-versioning.ts
app.get('/api/users', (req, res) => {
  const version = req.headers['api-version'] || '1';

  switch (version) {
    case '1':
      return res.json(transformToV1(users));
    case '2':
      return res.json(transformToV2(users));
    default:
      return res.status(400).json({ error: 'Unsupported API version' });
  }
});

// Or using Accept header
app.get('/api/users', (req, res) => {
  const acceptHeader = req.headers['accept'];

  if (acceptHeader.includes('application/vnd.myapi.v2+json')) {
    return res.json(transformToV2(users));
  }

  // Default to v1
  return res.json(transformToV1(users));
});
优点: URL简洁、符合REST规范 缺点: 可见性低、手动测试难度大

Query Parameter Versioning

查询参数版本控制

typescript
// query-param-versioning.ts
app.get('/api/users', (req, res) => {
  const version = req.query.version || '1';

  if (version === '2') {
    return res.json(transformToV2(users));
  }

  return res.json(transformToV1(users));
});

// Usage: GET /api/users?version=2
Pros: Easy to implement, flexible Cons: Not RESTful, can be overlooked
typescript
// query-param-versioning.ts
app.get('/api/users', (req, res) => {
  const version = req.query.version || '1';

  if (version === '2') {
    return res.json(transformToV2(users));
  }

  return res.json(transformToV1(users));
});

// Usage: GET /api/users?version=2
优点: 易于实现、灵活性高 缺点: 不符合REST规范、容易被忽略

2. Backward Compatibility Patterns

2. 向后兼容模式

Additive Changes (Non-Breaking)

增量变更(非破坏性)

typescript
// ✅ Safe: Adding optional fields
interface UserV1 {
  id: string;
  name: string;
}

interface UserV2 extends UserV1 {
  email?: string;  // Optional field
  avatar?: string; // Optional field
}

// ✅ Safe: Adding new endpoints
app.post('/api/v1/users/:id/avatar', uploadAvatar);

// ✅ Safe: Accepting additional parameters
app.get('/api/v1/users', (req, res) => {
  const { page, limit, sortBy } = req.query; // New optional params
  const users = await userService.list({ page, limit, sortBy });
  res.json(users);
});
typescript
// ✅ Safe: Adding optional fields
interface UserV1 {
  id: string;
  name: string;
}

interface UserV2 extends UserV1 {
  email?: string;  // Optional field
  avatar?: string; // Optional field
}

// ✅ Safe: Adding new endpoints
app.post('/api/v1/users/:id/avatar', uploadAvatar);

// ✅ Safe: Accepting additional parameters
app.get('/api/v1/users', (req, res) => {
  const { page, limit, sortBy } = req.query; // New optional params
  const users = await userService.list({ page, limit, sortBy });
  res.json(users);
});

Breaking Changes (Require New Version)

破坏性变更(需要新版本)

typescript
// ❌ Breaking: Removing fields
interface UserV1 {
  id: string;
  name: string;
  username: string;
}

interface UserV2 {
  id: string;
  name: string;
  // username removed - BREAKING!
}

// ❌ Breaking: Changing field types
interface UserV1 {
  id: string;
  created: string; // ISO string
}

interface UserV2 {
  id: string;
  created: number; // Unix timestamp - BREAKING!
}

// ❌ Breaking: Renaming fields
interface UserV1 {
  fullName: string;
}

interface UserV2 {
  name: string; // Renamed from fullName - BREAKING!
}

// ❌ Breaking: Changing response structure
// V1
{ users: [...], total: 10 }

// V2 - BREAKING!
{ data: [...], meta: { total: 10 } }
typescript
// ❌ Breaking: Removing fields
interface UserV1 {
  id: string;
  name: string;
  username: string;
}

interface UserV2 {
  id: string;
  name: string;
  // username removed - BREAKING!
}

// ❌ Breaking: Changing field types
interface UserV1 {
  id: string;
  created: string; // ISO string
}

interface UserV2 {
  id: string;
  created: number; // Unix timestamp - BREAKING!
}

// ❌ Breaking: Renaming fields
interface UserV1 {
  fullName: string;
}

interface UserV2 {
  name: string; // Renamed from fullName - BREAKING!
}

// ❌ Breaking: Changing response structure
// V1
{ users: [...], total: 10 }

// V2 - BREAKING!
{ data: [...], meta: { total: 10 } }

Handling Both Versions

多版本兼容处理

typescript
// version-adapter.ts
export class UserAdapter {
  toV1(user: User): UserV1Response {
    return {
      id: user.id,
      name: user.fullName,
      username: user.username,
      created: user.createdAt.toISOString()
    };
  }

  toV2(user: User): UserV2Response {
    return {
      id: user.id,
      name: user.fullName,
      email: user.email,
      profile: {
        avatar: user.avatarUrl,
        bio: user.bio
      },
      createdAt: user.createdAt.getTime()
    };
  }

  fromV1(data: UserV1Request): User {
    return {
      fullName: data.name,
      username: data.username,
      email: data.email || null
    };
  }

  fromV2(data: UserV2Request): User {
    return {
      fullName: data.name,
      username: data.username || generateUsername(data.email),
      email: data.email,
      avatarUrl: data.profile?.avatar,
      bio: data.profile?.bio
    };
  }
}

// Usage in controller
app.get('/api/:version/users/:id', async (req, res) => {
  const user = await userService.findById(req.params.id);
  const adapter = new UserAdapter();

  const response = req.params.version === 'v2'
    ? adapter.toV2(user)
    : adapter.toV1(user);

  res.json(response);
});
typescript
// version-adapter.ts
export class UserAdapter {
  toV1(user: User): UserV1Response {
    return {
      id: user.id,
      name: user.fullName,
      username: user.username,
      created: user.createdAt.toISOString()
    };
  }

  toV2(user: User): UserV2Response {
    return {
      id: user.id,
      name: user.fullName,
      email: user.email,
      profile: {
        avatar: user.avatarUrl,
        bio: user.bio
      },
      createdAt: user.createdAt.getTime()
    };
  }

  fromV1(data: UserV1Request): User {
    return {
      fullName: data.name,
      username: data.username,
      email: data.email || null
    };
  }

  fromV2(data: UserV2Request): User {
    return {
      fullName: data.name,
      username: data.username || generateUsername(data.email),
      email: data.email,
      avatarUrl: data.profile?.avatar,
      bio: data.profile?.bio
    };
  }
}

// Usage in controller
app.get('/api/:version/users/:id', async (req, res) => {
  const user = await userService.findById(req.params.id);
  const adapter = new UserAdapter();

  const response = req.params.version === 'v2'
    ? adapter.toV2(user)
    : adapter.toV1(user);

  res.json(response);
});

3. Deprecation Strategy

3. 弃用策略

Deprecation Headers

弃用请求头

typescript
// deprecation-middleware.ts
export function deprecationWarning(version: string, sunsetDate: Date) {
  return (req, res, next) => {
    res.setHeader('Deprecation', 'true');
    res.setHeader('Sunset', sunsetDate.toUTCString());
    res.setHeader('Link', '</api/v2/docs>; rel="successor-version"');
    res.setHeader('X-API-Warn', `Version ${version} is deprecated. Please migrate to v2 by ${sunsetDate.toDateString()}`);
    next();
  };
}

// Apply to deprecated routes
app.use('/api/v1/*', deprecationWarning('v1', new Date('2024-12-31')));

app.get('/api/v1/users', (req, res) => {
  // Return v1 response with deprecation headers
  res.json(users);
});
typescript
// deprecation-middleware.ts
export function deprecationWarning(version: string, sunsetDate: Date) {
  return (req, res, next) => {
    res.setHeader('Deprecation', 'true');
    res.setHeader('Sunset', sunsetDate.toUTCString());
    res.setHeader('Link', '</api/v2/docs>; rel="successor-version"');
    res.setHeader('X-API-Warn', `Version ${version} is deprecated. Please migrate to v2 by ${sunsetDate.toDateString()}`);
    next();
  };
}

// Apply to deprecated routes
app.use('/api/v1/*', deprecationWarning('v1', new Date('2024-12-31')));

app.get('/api/v1/users', (req, res) => {
  // Return v1 response with deprecation headers
  res.json(users);
});

Deprecation Response

弃用响应内容

typescript
// Include deprecation info in response body
app.get('/api/v1/users', (req, res) => {
  res.json({
    _meta: {
      deprecated: true,
      sunsetDate: '2024-12-31',
      message: 'This API version is deprecated. Please migrate to v2.',
      migrationGuide: 'https://docs.example.com/migration-v1-to-v2'
    },
    users: [...]
  });
});
typescript
// Include deprecation info in response body
app.get('/api/v1/users', (req, res) => {
  res.json({
    _meta: {
      deprecated: true,
      sunsetDate: '2024-12-31',
      message: 'This API version is deprecated. Please migrate to v2.',
      migrationGuide: 'https://docs.example.com/migration-v1-to-v2'
    },
    users: [...]
  });
});

Gradual Deprecation Timeline

渐进式弃用时间线

typescript
// deprecation-stages.ts
enum DeprecationStage {
  SUPPORTED = 'supported',
  DEPRECATED = 'deprecated',
  SUNSET_ANNOUNCED = 'sunset_announced',
  READONLY = 'readonly',
  SHUTDOWN = 'shutdown'
}

const versionStatus = {
  'v1': {
    stage: DeprecationStage.READONLY,
    sunsetDate: new Date('2024-06-30'),
    message: 'Read-only mode. New writes are disabled.'
  },
  'v2': {
    stage: DeprecationStage.DEPRECATED,
    sunsetDate: new Date('2024-12-31'),
    message: 'Deprecated. Please migrate to v3.'
  },
  'v3': {
    stage: DeprecationStage.SUPPORTED,
    message: 'Current stable version.'
  }
};

// Middleware to enforce deprecation
app.use('/api/:version/*', (req, res, next) => {
  const status = versionStatus[req.params.version];

  if (!status) {
    return res.status(404).json({ error: 'API version not found' });
  }

  if (status.stage === DeprecationStage.SHUTDOWN) {
    return res.status(410).json({ error: 'API version no longer available' });
  }

  if (status.stage === DeprecationStage.READONLY &&
      ['POST', 'PUT', 'PATCH', 'DELETE'].includes(req.method)) {
    return res.status(403).json({
      error: 'API version is read-only',
      message: status.message
    });
  }

  // Add deprecation headers
  if (status.stage !== DeprecationStage.SUPPORTED) {
    res.setHeader('X-API-Deprecated', 'true');
    res.setHeader('X-API-Sunset', status.sunsetDate.toISOString());
  }

  next();
});
typescript
// deprecation-stages.ts
enum DeprecationStage {
  SUPPORTED = 'supported',
  DEPRECATED = 'deprecated',
  SUNSET_ANNOUNCED = 'sunset_announced',
  READONLY = 'readonly',
  SHUTDOWN = 'shutdown'
}

const versionStatus = {
  'v1': {
    stage: DeprecationStage.READONLY,
    sunsetDate: new Date('2024-06-30'),
    message: 'Read-only mode. New writes are disabled.'
  },
  'v2': {
    stage: DeprecationStage.DEPRECATED,
    sunsetDate: new Date('2024-12-31'),
    message: 'Deprecated. Please migrate to v3.'
  },
  'v3': {
    stage: DeprecationStage.SUPPORTED,
    message: 'Current stable version.'
  }
};

// Middleware to enforce deprecation
app.use('/api/:version/*', (req, res, next) => {
  const status = versionStatus[req.params.version];

  if (!status) {
    return res.status(404).json({ error: 'API version not found' });
  }

  if (status.stage === DeprecationStage.SHUTDOWN) {
    return res.status(410).json({ error: 'API version no longer available' });
  }

  if (status.stage === DeprecationStage.READONLY &&
      ['POST', 'PUT', 'PATCH', 'DELETE'].includes(req.method)) {
    return res.status(403).json({
      error: 'API version is read-only',
      message: status.message
    });
  }

  // Add deprecation headers
  if (status.stage !== DeprecationStage.SUPPORTED) {
    res.setHeader('X-API-Deprecated', 'true');
    res.setHeader('X-API-Sunset', status.sunsetDate.toISOString());
  }

  next();
});

4. Migration Guide Example

4. 迁移指南示例

markdown
undefined
markdown
undefined

API Migration Guide: v1 to v2

API迁移指南:v1到v2

Overview

概述

Version 2 introduces breaking changes to improve consistency and add new features.
Timeline:
  • 2024-01-01: v2 released
  • 2024-06-01: v1 deprecated
  • 2024-09-01: v1 read-only
  • 2024-12-31: v1 shutdown
Version 2 introduces breaking changes to improve consistency and add new features.
时间线:
  • 2024-01-01: v2发布
  • 2024-06-01: v1弃用
  • 2024-09-01: v1进入只读模式
  • 2024-12-31: v1停止服务

Breaking Changes

破坏性变更

1. Response Structure

1. 响应结构

v1:
json
{
  "users": [...],
  "total": 10,
  "page": 1
}
v2:
json
{
  "data": [...],
  "meta": {
    "total": 10,
    "page": 1,
    "perPage": 20
  }
}
Migration:
javascript
// Before
const users = response.users;
const total = response.total;

// After
const users = response.data;
const total = response.meta.total;
v1:
json
{
  "users": [...],
  "total": 10,
  "page": 1
}
v2:
json
{
  "data": [...],
  "meta": {
    "total": 10,
    "page": 1,
    "perPage": 20
  }
}
迁移方式:
javascript
// Before
const users = response.users;
const total = response.total;

// After
const users = response.data;
const total = response.meta.total;

2. Date Format

2. 日期格式

v1: ISO 8601 strings v2: Unix timestamps
Migration:
javascript
// Before
const created = new Date(user.created);

// After
const created = new Date(user.created * 1000);
v1: ISO 8601字符串 v2: Unix时间戳
迁移方式:
javascript
// Before
const created = new Date(user.created);

// After
const created = new Date(user.created * 1000);

3. Error Format

3. 错误格式

v1:
json
{ "error": "User not found" }
v2:
json
{
  "error": {
    "code": "USER_NOT_FOUND",
    "message": "User not found",
    "details": {}
  }
}
v1:
json
{ "error": "User not found" }
v2:
json
{
  "error": {
    "code": "USER_NOT_FOUND",
    "message": "User not found",
    "details": {}
  }
}

New Features in v2

v2新增功能

Pagination

分页

javascript
// v2 supports cursor-based pagination
GET /api/v2/users?cursor=eyJpZCI6MTIzfQ&limit=20
javascript
// v2支持基于游标分页
GET /api/v2/users?cursor=eyJpZCI6MTIzfQ&limit=20

Field Selection

字段选择

javascript
// v2 supports field filtering
GET /api/v2/users?fields=id,name,email
javascript
// v2支持字段过滤
GET /api/v2/users?fields=id,name,email

Batch Operations

批量操作

javascript
// v2 supports batch requests
POST /api/v2/batch
{
  "requests": [
    { "method": "GET", "path": "/users/1" },
    { "method": "GET", "path": "/users/2" }
  ]
}
javascript
// v2支持批量请求
POST /api/v2/batch
{
  "requests": [
    { "method": "GET", "path": "/users/1" },
    { "method": "GET", "path": "/users/2" }
  ]
}

Code Examples

代码示例

JavaScript/TypeScript

JavaScript/TypeScript

typescript
// v1 Client
class ApiClientV1 {
  async getUsers() {
    const response = await fetch('/api/v1/users');
    const data = await response.json();
    return data.users;
  }
}

// v2 Client
class ApiClientV2 {
  async getUsers() {
    const response = await fetch('/api/v2/users');
    const data = await response.json();
    return data.data; // Changed from .users to .data
  }
}
typescript
// v1 Client
class ApiClientV1 {
  async getUsers() {
    const response = await fetch('/api/v1/users');
    const data = await response.json();
    return data.users;
  }
}

// v2 Client
class ApiClientV2 {
  async getUsers() {
    const response = await fetch('/api/v2/users');
    const data = await response.json();
    return data.data; // Changed from .users to .data
  }
}

Python

Python

python
undefined
python
undefined

v1

v1

response = requests.get(f"{base_url}/api/v1/users") users = response.json()["users"]
response = requests.get(f"{base_url}/api/v1/users") users = response.json()["users"]

v2

v2

response = requests.get(f"{base_url}/api/v2/users") users = response.json()["data"]
undefined
response = requests.get(f"{base_url}/api/v2/users") users = response.json()["data"]
undefined

5. GraphQL Versioning

5. GraphQL版本控制

typescript
// GraphQL handles versioning differently - through schema evolution
// schema-v1.graphql
type User {
  id: ID!
  name: String!
  username: String!
}

// schema-v2.graphql (deprecated fields)
type User {
  id: ID!
  name: String!
  username: String! @deprecated(reason: "Use email instead")
  email: String!
  profile: Profile
}

type Profile {
  avatar: String
  bio: String
}

// Field deprecation in resolver
const resolvers = {
  User: {
    username: (user) => {
      console.warn('username field is deprecated, use email instead');
      return user.email;
    }
  }
};
typescript
// GraphQL handles versioning differently - through schema evolution
// schema-v1.graphql
type User {
  id: ID!
  name: String!
  username: String!
}

// schema-v2.graphql (deprecated fields)
type User {
  id: ID!
  name: String!
  username: String! @deprecated(reason: "Use email instead")
  email: String!
  profile: Profile
}

type Profile {
  avatar: String
  bio: String
}

// Field deprecation in resolver
const resolvers = {
  User: {
    username: (user) => {
      console.warn('username field is deprecated, use email instead');
      return user.email;
    }
  }
};

6. gRPC Versioning

6. gRPC版本控制

protobuf
// v1/user.proto
syntax = "proto3";
package user.v1;

message User {
  string id = 1;
  string name = 2;
}

// v2/user.proto
syntax = "proto3";
package user.v2;

message User {
  string id = 1;
  string name = 2;
  string email = 3;
  Profile profile = 4;
}

message Profile {
  string avatar = 1;
  string bio = 2;
}

// Both versions can coexist
service UserServiceV1 {
  rpc GetUser (GetUserRequest) returns (user.v1.User);
}

service UserServiceV2 {
  rpc GetUser (GetUserRequest) returns (user.v2.User);
}
protobuf
// v1/user.proto
syntax = "proto3";
package user.v1;

message User {
  string id = 1;
  string name = 2;
}

// v2/user.proto
syntax = "proto3";
package user.v2;

message User {
  string id = 1;
  string name = 2;
  string email = 3;
  Profile profile = 4;
}

message Profile {
  string avatar = 1;
  string bio = 2;
}

// Both versions can coexist
service UserServiceV1 {
  rpc GetUser (GetUserRequest) returns (user.v1.User);
}

service UserServiceV2 {
  rpc GetUser (GetUserRequest) returns (user.v2.User);
}

7. Version Detection & Routing

7. 版本检测与路由

typescript
// version-router.ts
import express from 'express';

export class VersionRouter {
  private versions = new Map<string, express.Router>();

  registerVersion(version: string, router: express.Router) {
    this.versions.set(version, router);
  }

  getMiddleware() {
    return (req, res, next) => {
      // Detect version from multiple sources
      const version = this.detectVersion(req);

      const router = this.versions.get(version);
      if (!router) {
        return res.status(400).json({
          error: 'Invalid API version',
          supportedVersions: Array.from(this.versions.keys())
        });
      }

      // Set version in request for logging
      req.apiVersion = version;

      // Use versioned router
      router(req, res, next);
    };
  }

  private detectVersion(req): string {
    // 1. Check URL path
    const pathMatch = req.path.match(/^\/api\/v(\d+)\//);
    if (pathMatch) return pathMatch[1];

    // 2. Check header
    if (req.headers['api-version']) {
      return req.headers['api-version'];
    }

    // 3. Check Accept header
    const acceptMatch = req.headers['accept']?.match(/application\/vnd\.myapi\.v(\d+)\+json/);
    if (acceptMatch) return acceptMatch[1];

    // 4. Check query parameter
    if (req.query.version) {
      return req.query.version;
    }

    // 5. Default version
    return '1';
  }
}

// Usage
const versionRouter = new VersionRouter();

versionRouter.registerVersion('1', v1Router);
versionRouter.registerVersion('2', v2Router);
versionRouter.registerVersion('3', v3Router);

app.use('/api', versionRouter.getMiddleware());
typescript
// version-router.ts
import express from 'express';

export class VersionRouter {
  private versions = new Map<string, express.Router>();

  registerVersion(version: string, router: express.Router) {
    this.versions.set(version, router);
  }

  getMiddleware() {
    return (req, res, next) => {
      // Detect version from multiple sources
      const version = this.detectVersion(req);

      const router = this.versions.get(version);
      if (!router) {
        return res.status(400).json({
          error: 'Invalid API version',
          supportedVersions: Array.from(this.versions.keys())
        });
      }

      // Set version in request for logging
      req.apiVersion = version;

      // Use versioned router
      router(req, res, next);
    };
  }

  private detectVersion(req): string {
    // 1. Check URL path
    const pathMatch = req.path.match(/^\/api\/v(\d+)\//);
    if (pathMatch) return pathMatch[1];

    // 2. Check header
    if (req.headers['api-version']) {
      return req.headers['api-version'];
    }

    // 3. Check Accept header
    const acceptMatch = req.headers['accept']?.match(/application\/vnd\.myapi\.v(\d+)\+json/);
    if (acceptMatch) return acceptMatch[1];

    // 4. Check query parameter
    if (req.query.version) {
      return req.query.version;
    }

    // 5. Default version
    return '1';
  }
}

// Usage
const versionRouter = new VersionRouter();

versionRouter.registerVersion('1', v1Router);
versionRouter.registerVersion('2', v2Router);
versionRouter.registerVersion('3', v3Router);

app.use('/api', versionRouter.getMiddleware());

8. Testing Multiple Versions

8. 多版本测试

typescript
// api-version.test.ts
describe('API Versioning', () => {
  describe('v1', () => {
    it('should return user with v1 format', async () => {
      const response = await request(app)
        .get('/api/v1/users/1')
        .expect(200);

      expect(response.body).toHaveProperty('id');
      expect(response.body).toHaveProperty('name');
      expect(response.body).not.toHaveProperty('email');
    });
  });

  describe('v2', () => {
    it('should return user with v2 format', async () => {
      const response = await request(app)
        .get('/api/v2/users/1')
        .expect(200);

      expect(response.body).toHaveProperty('id');
      expect(response.body).toHaveProperty('name');
      expect(response.body).toHaveProperty('email');
      expect(response.body).toHaveProperty('profile');
    });

    it('should include deprecation headers for v1', async () => {
      const response = await request(app)
        .get('/api/v1/users/1');

      expect(response.headers['deprecation']).toBe('true');
      expect(response.headers['sunset']).toBeDefined();
    });
  });

  describe('version negotiation', () => {
    it('should use version from header', async () => {
      const response = await request(app)
        .get('/api/users/1')
        .set('API-Version', '2')
        .expect(200);

      expect(response.body).toHaveProperty('email');
    });

    it('should default to v1 if no version specified', async () => {
      const response = await request(app)
        .get('/api/users/1')
        .expect(200);

      expect(response.body).not.toHaveProperty('email');
    });
  });
});
typescript
// api-version.test.ts
describe('API Versioning', () => {
  describe('v1', () => {
    it('should return user with v1 format', async () => {
      const response = await request(app)
        .get('/api/v1/users/1')
        .expect(200);

      expect(response.body).toHaveProperty('id');
      expect(response.body).toHaveProperty('name');
      expect(response.body).not.toHaveProperty('email');
    });
  });

  describe('v2', () => {
    it('should return user with v2 format', async () => {
      const response = await request(app)
        .get('/api/v2/users/1')
        .expect(200);

      expect(response.body).toHaveProperty('id');
      expect(response.body).toHaveProperty('name');
      expect(response.body).toHaveProperty('email');
      expect(response.body).toHaveProperty('profile');
    });

    it('should include deprecation headers for v1', async () => {
      const response = await request(app)
        .get('/api/v1/users/1');

      expect(response.headers['deprecation']).toBe('true');
      expect(response.headers['sunset']).toBeDefined();
    });
  });

  describe('version negotiation', () => {
    it('should use version from header', async () => {
      const response = await request(app)
        .get('/api/users/1')
        .set('API-Version', '2')
        .expect(200);

      expect(response.body).toHaveProperty('email');
    });

    it('should default to v1 if no version specified', async () => {
      const response = await request(app)
        .get('/api/users/1')
        .expect(200);

      expect(response.body).not.toHaveProperty('email');
    });
  });
});

Best Practices

最佳实践

✅ DO

✅ 推荐做法

  • Version from day one (even if v1)
  • Document breaking vs non-breaking changes
  • Provide clear migration guides with code examples
  • Use semantic versioning principles
  • Give 6-12 months deprecation notice
  • Monitor usage of deprecated APIs
  • Send deprecation warnings to API consumers
  • Support at least 2 versions simultaneously
  • Use adapters/transformers for version logic
  • Test all supported versions
  • Log which API version is being used
  • Provide migration tooling when possible
  • Be consistent with versioning approach
  • 从项目初期就引入版本控制(即使是v1)
  • 明确文档化破坏性与非破坏性变更
  • 提供包含代码示例的清晰迁移指南
  • 遵循语义化版本控制原则
  • 给出6-12个月的弃用通知期
  • 监控旧版API的使用情况
  • 向API使用者发送弃用警告
  • 同时支持至少2个版本
  • 使用适配器/转换器处理版本逻辑
  • 测试所有受支持的版本
  • 记录正在使用的API版本
  • 尽可能提供迁移工具
  • 保持版本控制方法的一致性

❌ DON'T

❌ 不推荐做法

  • Change API behavior without versioning
  • Remove versions without notice
  • Support too many versions (>3)
  • Use different versioning strategies in same API
  • Break APIs without incrementing version
  • Forget to update documentation
  • Deprecate too quickly (<6 months)
  • Ignore feedback from API consumers
  • Make every change a new version
  • Use version numbers inconsistently
  • 不进行版本控制就修改API行为
  • 未提前通知就移除版本
  • 支持过多版本(>3个)
  • 在同一API中使用不同的版本控制策略
  • 不升级版本就破坏API
  • 忘记更新文档
  • 过快弃用(<6个月)
  • 忽略API使用者的反馈
  • 每次变更都创建新版本
  • 版本号使用不一致

Common Patterns

常见模式

Pattern 1: Version-Agnostic Core

模式1:版本无关核心逻辑

typescript
// Core logic remains version-agnostic
class UserService {
  async getUser(id: string): Promise<User> {
    return this.repository.findById(id);
  }
}

// Version-specific adapters
class UserV1Adapter {
  transform(user: User): UserV1 { /* ... */ }
}

class UserV2Adapter {
  transform(user: User): UserV2 { /* ... */ }
}
typescript
// Core logic remains version-agnostic
class UserService {
  async getUser(id: string): Promise<User> {
    return this.repository.findById(id);
  }
}

// Version-specific adapters
class UserV1Adapter {
  transform(user: User): UserV1 { /* ... */ }
}

class UserV2Adapter {
  transform(user: User): UserV2 { /* ... */ }
}

Pattern 2: Feature Flags for Gradual Rollout

模式2:功能标记渐进式发布

typescript
app.get('/api/v2/users', async (req, res) => {
  const user = await userService.getUser(req.params.id);

  // Gradual rollout of new feature
  if (featureFlags.isEnabled('enhanced-profile', req.user.id)) {
    return res.json(transformWithEnhancedProfile(user));
  }

  return res.json(transformV2(user));
});
typescript
app.get('/api/v2/users', async (req, res) => {
  const user = await userService.getUser(req.params.id);

  // Gradual rollout of new feature
  if (featureFlags.isEnabled('enhanced-profile', req.user.id)) {
    return res.json(transformWithEnhancedProfile(user));
  }

  return res.json(transformV2(user));
});

Pattern 3: API Version Metrics

模式3:API版本指标监控

typescript
// Track usage by version
app.use((req, res, next) => {
  const version = detectVersion(req);
  metrics.increment('api.requests', { version });
  next();
});
typescript
// Track usage by version
app.use((req, res, next) => {
  const version = detectVersion(req);
  metrics.increment('api.requests', { version });
  next();
});

Tools & Resources

工具与资源

  • OpenAPI/Swagger: API documentation with version support
  • Postman: API testing with version management
  • API Blueprint: API design with versioning
  • Stoplight: API design and documentation
  • Kong: API gateway with version routing
  • OpenAPI/Swagger: 支持版本的API文档工具
  • Postman: 具备版本管理的API测试工具
  • API Blueprint: 支持版本控制的API设计工具
  • Stoplight: API设计与文档平台
  • Kong: 支持版本路由的API网关