API 版本控制是 API 设计中最重要的架构决策之一。一旦客户端依赖你的 API,在没有迁移计划的情况下,你永远不能破坏向后兼容性。版本控制让你可以在保持现有客户端功能的同时演进 API。本指南涵盖所有主要版本控制策略,包括实现示例、权衡取舍和弃用最佳实践。
为什么要版本化 API?
- 破坏性更改(重命名字段、更改响应结构)需要新版本
- 添加新的必需参数或更改认证方案
- 不同客户端(移动端、Web、第三方)可能需要在不同时间迁移
- 服务 SLA 可能要求在弃用端点前提前几个月通知
URL 路径版本控制
版本号直接嵌入 URL 路径中。这是最广泛使用的方法,也是最明显的——你可以在每个请求中看到版本。
# URL Path Versioning — the most common approach
# Endpoints
GET /api/v1/users
POST /api/v1/users
GET /api/v1/users/123
GET /api/v2/users
GET /api/v2/users/123
# Express.js implementation
const express = require('express');
const app = express();
// v1 routes
const v1Router = express.Router();
v1Router.get('/users', (req, res) => {
res.json({ version: 'v1', users: [{ id: 1, name: 'Alice' }] });
});
v1Router.get('/users/:id', (req, res) => {
res.json({ id: req.params.id, name: 'Alice', email: 'alice@example.com' });
});
// v2 routes (different response shape)
const v2Router = express.Router();
v2Router.get('/users', (req, res) => {
res.json({
version: 'v2',
data: [{ id: 1, firstName: 'Alice', lastName: 'Smith' }], // different field names
meta: { total: 1, page: 1 }
});
});
v2Router.get('/users/:id', (req, res) => {
res.json({
id: req.params.id,
firstName: 'Alice', // renamed from 'name'
lastName: 'Smith',
contact: { email: 'alice@example.com' } // nested structure
});
});
app.use('/api/v1', v1Router);
app.use('/api/v2', v2Router);
// Catch-all: redirect unversioned to latest
app.use('/api/users', (req, res) => {
res.redirect(308, `/api/v2/users${req.path}`);
});基于请求头的版本控制
版本在请求头(自定义头或 Accept 头)中指定。URL 在版本之间保持稳定。
# Header-Based Versioning — cleaner URLs, more complex clients
# Using a custom header
GET /api/users
API-Version: 2
# Or using Accept header (media type versioning)
GET /api/users
Accept: application/vnd.myapi.v2+json
# Or using content negotiation per GitHub's approach
GET /api/users
Accept: application/vnd.github.v3+json
# Express.js implementation
const express = require('express');
const app = express();
// Version middleware — extracts version from header
function versionMiddleware(req, res, next) {
const version = req.headers['api-version'] ||
extractVersionFromAccept(req.headers['accept']) ||
'1'; // default to v1
req.apiVersion = parseInt(version, 10);
next();
}
function extractVersionFromAccept(accept) {
// Parse: application/vnd.myapi.v2+json → 2
const match = accept && accept.match(/vnd\.myapi\.v(\d+)\+json/);
return match ? match[1] : null;
}
app.use(versionMiddleware);
app.get('/api/users', (req, res) => {
if (req.apiVersion >= 2) {
return res.json({
data: [{ id: 1, firstName: 'Alice', lastName: 'Smith' }],
meta: { total: 1 }
});
}
// v1 response
res.json([{ id: 1, name: 'Alice Smith' }]);
});查询参数版本控制
版本作为查询参数传递。简单,但通常不推荐用于生产 API。
# Query Parameter Versioning — simple but often considered messy
GET /api/users?version=2
GET /api/users?v=2
GET /api/users?api-version=2 # Azure REST API style
# Python FastAPI implementation
from fastapi import FastAPI, Query
from typing import Optional
app = FastAPI()
@app.get("/api/users")
async def get_users(version: Optional[int] = Query(default=1, ge=1, le=2)):
if version == 2:
return {
"data": [{"id": 1, "firstName": "Alice", "lastName": "Smith"}],
"meta": {"total": 1}
}
# Default v1
return [{"id": 1, "name": "Alice Smith"}]
# Azure REST API uses api-version query param universally
# GET https://management.azure.com/subscriptions/{id}/resourceGroups?api-version=2024-01-01弃用和日落策略
版本控制最重要的部分是清晰的弃用生命周期。客户端需要时间来迁移。
# Deprecation and Sunset Strategy
# Critical for managing API lifecycle
# Response headers indicating deprecation
HTTP/1.1 200 OK
Deprecation: true
Sunset: Sat, 31 Dec 2026 23:59:59 GMT
Link: <https://api.example.com/v2/users>; rel="successor-version"
Warning: 299 - "API v1 is deprecated. Migrate to v2 by 2026-12-31."
# Express.js deprecation middleware
function deprecationMiddleware(sunsetDate, successorPath) {
return (req, res, next) => {
res.set({
'Deprecation': 'true',
'Sunset': new Date(sunsetDate).toUTCString(),
'Link': `<https://api.example.com${successorPath}>; rel="successor-version"`,
'Warning': `299 - "This API version is deprecated. Please migrate by ${sunsetDate}"`
});
next();
};
}
// Apply to all v1 routes
app.use('/api/v1',
deprecationMiddleware('2026-12-31', '/api/v2'),
v1Router
);
# Versioning in OpenAPI/Swagger
openapi: "3.1.0"
info:
title: My API
version: "2.0.0" # API version (not OpenAPI spec version)
servers:
- url: https://api.example.com/v2
description: Production v2
- url: https://api.example.com/v1
description: v1 (deprecated, sunset 2026-12-31)GraphQL:模式演进而非版本
GraphQL 采用不同的方法——不是对整个 API 进行版本控制,而是在添加新字段的同时弃用单个字段。这就是为什么 GraphQL API 很少需要显式版本。
# GraphQL: versioning via schema evolution (no versions needed)
# GraphQL avoids versioning by:
# 1. Adding new fields (backwards compatible)
# 2. Deprecating old fields with @deprecated directive
# 3. Never removing fields until usage is zero
type User {
id: ID!
name: String @deprecated(reason: "Use firstName and lastName instead")
firstName: String! # new field
lastName: String! # new field
email: String!
contact: Contact # new nested type
}
type Contact {
email: String!
phone: String
}
# Query: clients using old 'name' field still work
query GetUser {
user(id: "123") {
name # deprecated but still works
firstName # new
email
}
}
# Monitoring: track deprecated field usage before removal
# Use Apollo Studio or schema field usage analytics版本控制策略对比
| Strategy | Pros | Cons | Example | Best For |
|---|---|---|---|---|
| URL Path (/v2/) | Explicit, cacheable, bookmarkable, easy to test | Version in URL feels "dirty" to REST purists | /api/v2/users | Public APIs, open source |
| Custom Header | Clean URLs, explicit versioning | Less visible, harder to test in browsers | API-Version: 2 | Enterprise internal APIs |
| Accept Header | HTTP standard, content negotiation | Complex to implement and test | Accept: vnd.api.v2+json | RESTful purists |
| Query Parameter | Simple, no path changes | Breaks REST semantics, bad for caching | /api/users?v=2 | Internal tools, prototypes |
| No versioning (GraphQL) | Single endpoint, schema evolution | Complex schema management | POST /graphql | GraphQL APIs |
最佳实践
- 绝不在没有至少 6 个月迁移窗口(企业 API 12 个月以上)的情况下破坏现有版本化 API。
- 使用 Sunset 请求头以编程方式向 API 客户端传达生命周期结束日期。
- 为每个 API 版本维护变更日志,记录更改内容和原因。
- 考虑语义版本控制:只针对破坏性更改递增主版本。次要版本可以添加新端点而不递增 URL 版本。
- 发布新主版本时,提供包含代码示例的迁移指南。
常见问题
哪种 API 版本控制策略最好?
URL 路径版本控制(/api/v2/resource)是公共 API 中最广泛推荐的,因为它明确、对缓存友好,并且易于在浏览器和 curl 中测试。当你想要简洁的 URL 且不介意要求客户端设置请求头时,首选请求头版本控制。查询参数对内部 API 是可以接受的。
什么算作破坏性更改?
破坏性更改包括:删除或重命名字段、更改字段类型(字符串到整数)、使可选字段变为必需、删除端点、更改认证方案、改变分页行为、更改错误响应格式。非破坏性更改:添加新的可选字段、添加新端点、添加新的可选查询参数、向现有端点添加新 HTTP 方法。
我应该从 v0 还是 v1 开始版本控制?
公共 API 从 v1 开始。v0 暗示不稳定,会阻止采用。对于真正不稳定的 API,使用私有测试版或预发布标志。如果你需要表示重大架构重构,直接跳到 v2。语义版本控制(v1.2.3)对于在主版本内传达更改范围很有用。
我应该维护弃用的 API 版本多长时间?
对于公共/第三方 API:最少 12 个月,最好 18-24 个月。对于移动应用:更长时间,因为用户可能不更新应用。对于内部 API:3-6 个月很常见。使用分析追踪实际使用情况——当使用率降至接近零时,你可以以最小干扰停用。绝不退役仍有大量流量的 API。
如何在 GraphQL 中处理破坏性更改的版本控制?
GraphQL 建议模式演进而非版本控制:(1) 在旧字段旁添加新字段,(2) 用 @deprecated 标记旧字段,(3) 通过 Apollo Studio 或自定义指标监控弃用字段的使用,(4) 只有在使用率达到零后才删除弃用字段。对于真正不兼容的更改,在 /graphql/v1 和 /graphql/v2 上并行运行模式。