跳到主要内容

REST API 版本控制

API版本控制是API生命周期管理的重要组成部分。随着业务发展,API不可避免地需要演进和变更。良好的版本控制策略可以让你在不破坏现有客户端的情况下,持续改进API。

为什么需要版本控制

API发布后,你无法控制谁在使用它、如何使用它。当需要做以下变更时,版本控制变得尤为重要:

破坏性变更:会导致现有客户端失败的变更

  • 删除端点或字段
  • 重命名端点或字段
  • 修改数据类型
  • 修改认证机制
  • 修改错误响应格式

非破坏性变更:不会影响现有客户端

  • 添加新的端点
  • 添加新的可选参数
  • 添加新的响应字段
  • 添加新的错误类型

版本控制的核心目标是:让破坏性变更成为新版本,让非破坏性变更向后兼容

版本控制策略

URL路径版本控制

在URL路径中包含版本号,这是最常用也最直观的方式。

格式

/api/v1/users
/api/v2/users
/api/v3/users

优点

  • 简单直观,一眼就能看出版本
  • 方便浏览器直接访问和测试
  • 支持CDN缓存
  • 容易路由和代理

缺点

  • URL变长
  • 版本切换需要修改URL
  • 不符合"URL表示资源"的纯粹REST理念

实现示例(Express.js):

// 方式1:使用路由前缀
const v1Router = require('./routes/v1');
const v2Router = require('./routes/v2');

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

// 方式2:使用路由参数
app.use('/api/:version', (req, res, next) => {
req.apiVersion = req.params.version;
next();
}, apiRouter);

// 路由处理器中根据版本选择逻辑
function getUsers(req, res) {
if (req.apiVersion === 'v1') {
return res.json(formatV1Users(users));
}
return res.json(formatV2Users(users));
}

实现示例(Spring Boot):

@RestController
@RequestMapping("/api/v1/users")
public class UserV1Controller {

@GetMapping
public List<UserV1> getUsers() {
return userService.getUsersV1();
}
}

@RestController
@RequestMapping("/api/v2/users")
public class UserV2Controller {

@GetMapping
public List<UserV2> getUsers() {
return userService.getUsersV2();
}
}

查询参数版本控制

通过查询参数指定版本号。

格式

/api/users?version=1
/api/users?version=2

优点

  • URL简洁,资源路径不变
  • 版本切换灵活
  • 默认版本容易实现

缺点

  • 容易被忽略
  • URL参数混杂,可能与其他参数冲突
  • 缓存策略需要额外处理

实现示例

// 版本控制中间件
function versionMiddleware(req, res, next) {
// 从查询参数获取版本
const version = req.query.version || '1';

// 验证版本号
const validVersions = ['1', '2', '3'];
if (!validVersions.includes(version)) {
return res.status(400).json({
error: '不支持的版本',
supportedVersions: validVersions
});
}

req.apiVersion = parseInt(version);
next();
}

app.use(versionMiddleware);

// 在处理器中使用版本
app.get('/users', (req, res) => {
const users = userService.getUsers();

if (req.apiVersion === 1) {
return res.json({
users: users.map(u => ({
id: u.id,
name: u.fullName,
email: u.email
}))
});
}

// v2版本返回更多字段
return res.json({
users: users.map(u => ({
id: u.id,
firstName: u.firstName,
lastName: u.lastName,
email: u.email,
avatar: u.avatar,
createdAt: u.createdAt
}))
});
});

HTTP头部版本控制

通过自定义HTTP头部指定版本号。

格式

GET /api/users HTTP/1.1
Host: api.example.com
Accept-Version: 2

或使用自定义头部:

GET /api/users HTTP/1.1
Host: api.example.com
X-API-Version: 2

优点

  • URL保持干净
  • 专业、符合HTTP规范
  • 便于API网关处理

缺点

  • 浏览器调试不便(需要额外工具)
  • 文档需要额外说明
  • 新开发者可能不容易发现

实现示例

function versionMiddleware(req, res, next) {
// 从头部获取版本
const version = req.headers['x-api-version'] ||
req.headers['accept-version'] ||
'1';

req.apiVersion = parseInt(version);
next();
}

app.use(versionMiddleware);

Accept头部版本控制(媒体类型)

使用Accept头部进行内容协商,这是最符合REST原则的方式。

格式

GET /api/users HTTP/1.1
Host: api.example.com
Accept: application/vnd.myapi.v2+json

优点

  • 最符合REST原则
  • URL保持干净
  • 支持同一资源不同版本返回不同格式
  • 灵活的内容协商

缺点

  • 实现复杂
  • 不便于浏览器测试
  • 开发者需要理解媒体类型概念

实现示例

function mediaTypeVersionMiddleware(req, res, next) {
const accept = req.headers.accept || 'application/json';

// 解析媒体类型中的版本
const versionMatch = accept.match(/application\/vnd\.\w+\.v(\d+)\+json/);

if (versionMatch) {
req.apiVersion = parseInt(versionMatch[1]);
} else {
req.apiVersion = 1; // 默认版本
}

// 设置响应内容类型
res.set('Content-Type', `application/vnd.myapi.v${req.apiVersion}+json`);

next();
}

app.use(mediaTypeVersionMiddleware);

策略对比

策略易用性RESTful缓存友好调试方便推荐场景
URL路径★★★★★★★★★★★★★★★★★★公开API、初学者团队
查询参数★★★★★★★★★★★★★★★内部API、快速迭代
自定义头部★★★★★★★★★★★★★企业API、API网关
Accept头部★★★★★★★★★★★严格REST、高级用户

语义化版本控制

语义化版本控制(Semantic Versioning)使用三段式版本号:MAJOR.MINOR.PATCH

版本号含义

  • MAJOR(主版本):不兼容的API变更
  • MINOR(次版本):向后兼容的功能新增
  • PATCH(补丁版本):向后兼容的问题修复

示例

v1.0.0 → v1.0.1  修复bug,不影响API
v1.0.1 → v1.1.0 新增可选参数或新端点
v1.1.0 → v2.0.0 删除端点或修改响应格式

在API中通常只暴露主版本号:

/api/v1/users  对应 v1.x.x
/api/v2/users 对应 v2.x.x

版本生命周期管理

版本支持策略

v1 发布 ──────────────────────────────────────────────→ 废弃
↑ ↑ ↑ ↑
发布 推荐v2 停止维护 强制升级
│ │ │ │
├─ 活跃期 ─────┼─ 维护期 ─────┼─ 废弃期 ─────┤
│ 新功能 │ 仅修复bug │ 仅安全补丁 │
│ │ │ │
0个月 12个月 18个月 24个月

版本状态

状态含义支持
Current当前推荐版本全功能支持
Supported旧版本,仍在支持Bug修复、安全补丁
Deprecated已废弃,即将下线仅安全补丁
Retired已下线无支持

废弃通知机制

// 在响应头中添加废弃警告
app.use((req, res, next) => {
if (req.apiVersion === 1 && isDeprecatedVersion(1)) {
res.set('X-API-Warn', 'API v1 is deprecated. Migrate to v2 by 2024-12-31');
res.set('X-API-Sunset', 'Sat, 31 Dec 2024 23:59:59 GMT');
res.set('Link', '</api/v2/users>; rel="successor-version"');
}
next();
});

响应体中包含废弃信息

{
"data": [...],
"deprecation": {
"deprecated": true,
"message": "API v1将于2024年12月31日下线",
"sunset": "2024-12-31T23:59:59Z",
"migrationGuide": "https://docs.example.com/migration/v1-to-v2"
}
}

迁移指南

为每个版本升级提供详细的迁移指南:

## 从 v1 迁移到 v2

### 端点变更

| v1 端点 | v2 端点 | 说明 |
|---------|---------|------|
| GET /users | GET /users | 响应格式变更 |
| GET /user/:id | GET /users/:id | URL规范化 |
| POST /user | POST /users | URL规范化 |

### 响应格式变更

**v1响应**
```json
{
"id": 123,
"name": "张三",
"email": "[email protected]"
}

v2响应

{
"id": 123,
"firstName": "三",
"lastName": "张",
"email": "[email protected]",
"createdAt": "2024-01-01T00:00:00Z"
}

字段映射

v1 字段v2 字段变更说明
namefirstName + lastName拆分为两个字段

## 实践建议

### 何时需要新版本

**需要新版本的情况**:

- 删除任何端点
- 删除或重命名响应字段
- 修改字段的语义
- 修改认证或授权机制
- 修改错误响应格式

**不需要新版本的情况**:

- 添加新的端点
- 添加新的可选请求参数
- 添加新的响应字段
- 修复bug
- 性能优化

### 向后兼容的设计

**使用可选参数而非必填参数**:

```javascript
// 好的设计:新参数可选
GET /api/users?includeDeleted=true

// 不好的设计:新参数必填
GET /api/users?status=active // 旧客户端不知道这个参数

使用默认值

// 新增的分页参数设置默认值
function getUsers(req, res) {
const page = parseInt(req.query.page) || 1;
const limit = parseInt(req.query.limit) || 20;

// 旧客户端不传参数也能正常工作
}

保留旧字段的同时添加新字段

{
"id": 123,
"name": "张三", // 保留旧字段
"firstName": "三", // 新增字段
"lastName": "张", // 新增字段
"email": "[email protected]"
}

版本路由最佳实践

集中管理版本

// config/versions.js
module.exports = {
current: 2,
supported: [1, 2],
deprecated: [1],
defaultVersion: 2,

routes: {
v1: require('./routes/v1'),
v2: require('./routes/v2')
}
};

// app.js
const { routes, defaultVersion } = require('./config/versions');

Object.entries(routes).forEach(([version, router]) => {
app.use(`/api/${version}`, router);
});

// 默认版本路由
app.use('/api', routes[`v${defaultVersion}`]);

版本特定的中间件

// 每个版本可以有不同的中间件链
const v1Middleware = [authV1, rateLimitV1];
const v2Middleware = [authV2, rateLimitV2, caching];

app.use('/api/v1', ...v1Middleware, v1Router);
app.use('/api/v2', ...v2Middleware, v2Router);

文档与版本

每个版本的API应该有独立的文档:

/docs/v1    → v1版本文档(标记已废弃)
/docs/v2 → v2版本文档(当前版本)
/docs → 重定向到最新版本文档

总结

REST API版本控制的关键要点:

  1. 选择适合的策略:根据团队和用户特点选择URL路径、查询参数、头部或媒体类型版本控制

  2. 遵循语义化版本:主版本用于破坏性变更,次版本用于兼容性新增

  3. 规划版本生命周期:明确支持期限,提前通知废弃

  4. 保持向后兼容:尽量通过新增而非修改来演进API

  5. 提供迁移指南:帮助用户平滑升级

记住:"API是承诺"。一旦发布,就要对用户负责。良好的版本控制策略可以让API在演进过程中保持稳定和可信赖。