API Versioning Patterns
Evolve your API confidently. Version correctly, deprecate gracefully, migrate safely — without breaking existing consumers.
Versioning Strategies
Pick one strategy and apply it consistently across your entire API surface.
| Strategy | Format | Visibility | Cacheability | Best For |
|---|
| URL Path | INLINECODE0 | High | Excellent | Public APIs, third-party integrations |
| Query Param |
/api/users?v=1 | Medium | Moderate | Simple APIs, prototyping |
|
Header |
Accept-Version: v1 | Low | Good | Internal APIs, coordinated consumers |
|
Content Negotiation |
Accept: application/vnd.api.v1+json | Low | Good | Enterprise, strict REST compliance |
URL Path Versioning
The most common strategy. Version lives in the URL, making it immediately visible.
CODEBLOCK0
Rules:
- - Always prefix:
/api/v1/... not INLINECODE5 - Major version only:
/api/v1/, never /api/v1.2/ or INLINECODE8 - Every endpoint must be versioned — no mixing versioned and unversioned paths
Header Versioning
Version specified via request headers, keeping URLs clean.
CODEBLOCK1
Always define fallback behavior when no version header is sent — default to latest stable or return 400 Bad Request.
Semantic Versioning for APIs
| SemVer Component | API Meaning | Action Required |
|---|
| MAJOR (v1 → v2) | Breaking changes — remove field, rename endpoint, change auth | Clients must migrate |
| MINOR (v1.1 → v1.2) |
Additive, backward-compatible — new optional field, new endpoint | No client changes |
|
PATCH (v1.1.0 → v1.1.1) | Bug fixes, no behavior change | No client changes |
Only MAJOR versions appear in URL paths. Communicate MINOR and PATCH through changelogs.
Breaking vs Non-Breaking Changes
Breaking — Require New Version
| Change | Why It Breaks |
|---|
| Remove a response field | Clients reading that field get INLINECODE10 |
| Rename a field |
Same as removal from the client's perspective |
| Change a field's type |
"id": 123 →
"id": "123" breaks typed clients |
| Remove an endpoint | Clients calling it get
404 |
| Make optional param required | Existing requests missing it start failing |
| Change URL structure | Bookmarked/hardcoded URLs break |
| Change error response format | Client error-handling logic breaks |
| Change authentication mechanism | Existing credentials stop working |
Non-Breaking — Safe Under Same Version
| Change | Why It's Safe |
|---|
| Add new optional response field | Clients ignore unknown fields |
| Add new endpoint |
Doesn't affect existing endpoints |
| Add new optional query/body param | Existing requests work without it |
| Add new enum value | Safe if clients handle unknown values gracefully |
| Relax a validation constraint | Previously valid requests remain valid |
| Improve performance | Same interface, faster response |
Deprecation Strategy
Never remove a version without warning. Follow this timeline:
CODEBLOCK2
Minimum deprecation periods: Public API: 12 months · Partner API: 6 months · Internal API: 1–3 months
Sunset HTTP Header (RFC 8594)
Include on every response from a deprecated version:
CODEBLOCK3
Retired Version Response
When past sunset, return 410 Gone:
CODEBLOCK4
Migration Patterns
Adapter Pattern
Shared business logic, version-specific serialization:
CODEBLOCK5
Facade Pattern
Single entry point delegates to the correct versioned handler:
CODEBLOCK6
Versioned Controllers
Separate controller files per version, shared service layer:
CODEBLOCK7
API Gateway Routing
Route versions at infrastructure layer:
CODEBLOCK8
Multi-Version Support
Architecture:
CODEBLOCK9
Principles:
- 1. Business logic is version-agnostic. Services, repositories, and domain models are shared.
- Serialization is version-specific. Each version has its own request validators and response serializers.
- Transformations are explicit. A
v1_to_v2 transformer documents every field mapping. - Tests cover all active versions. Every supported version has its own integration test suite.
Maximum concurrent versions: 2–3 active (current + 1–2 deprecated). More than 3 creates unsustainable maintenance burden.
Client Communication
Changelog
Publish a changelog for every release, tagged by version and change type:
CODEBLOCK10
Migration Guides
For every major version bump, provide:
- - Field-by-field mapping table (v1 → v2)
- Before/after request and response examples
- Code snippets for common languages/SDKs
- Timeline with key dates (announcement, sunset, removal)
SDK Versioning
Align SDK major versions with API major versions:
CODEBLOCK11
Ship the new SDK before announcing API deprecation.
Anti-Patterns
| Anti-Pattern | Fix |
|---|
| Versioning too frequently | Batch breaking changes into infrequent major releases |
| Breaking without notice |
Always follow the deprecation timeline |
|
Eternal version support | Set and enforce sunset dates |
|
Inconsistent versioning | One version scheme, applied uniformly |
|
Version per endpoint | Version the entire API surface together |
|
Using versions to gate features | Use feature flags separately; versions are for contracts |
|
No default version | Always define a default or return explicit 400 |
NEVER Do
- 1. NEVER remove a field, endpoint, or change a type without bumping the major version
- NEVER sunset a public API version with less than 6 months notice
- NEVER mix versioning strategies in the same API (URL path for some, headers for others)
- NEVER use minor or patch versions in URL paths (
/api/v1.2/ is wrong — use /api/v1/) - NEVER version individual endpoints independently — version the entire API surface as a unit
- NEVER deploy a breaking change under an existing version number, even if "nobody uses that field"
- NEVER skip documenting differences between versions — every breaking change needs a migration guide entry
API 版本控制模式
自信地演进你的 API。正确地进行版本控制、优雅地弃用、安全地迁移——而不会破坏现有消费者。
版本控制策略
选择一种策略,并在整个 API 表面一致地应用它。
| 策略 | 格式 | 可见性 | 可缓存性 | 最佳适用场景 |
|---|
| URL 路径 | /api/v1/users | 高 | 极佳 | 公共 API、第三方集成 |
| 查询参数 |
/api/users?v=1 | 中 | 中等 | 简单 API、原型开发 |
|
请求头 | Accept-Version: v1 | 低 | 良好 | 内部 API、协调的消费者 |
|
内容协商 | Accept: application/vnd.api.v1+json | 低 | 良好 | 企业级、严格的 REST 合规 |
URL 路径版本控制
最常见的策略。版本位于 URL 中,使其立即可见。
python
from fastapi import FastAPI, APIRouter
v1 = APIRouter(prefix=/api/v1)
v2 = APIRouter(prefix=/api/v2)
@v1.get(/users)
async def listusersv1():
return {users: [...]}
@v2.get(/users)
async def listusersv2():
return {data: {users: [...]}, meta: {...}}
app = FastAPI()
app.include_router(v1)
app.include_router(v2)
规则:
- - 始终添加前缀:/api/v1/... 而非 /v1/api/...
- 仅主版本号:/api/v1/,绝不用 /api/v1.2/ 或 /api/v1.2.3/
- 每个端点都必须有版本号——不要混用有版本和无版本的路径
请求头版本控制
通过请求头指定版本,保持 URL 整洁。
javascript
function versionRouter(req, res, next) {
const version = req.headers[accept-version] || v2; // 默认使用最新版本
req.apiVersion = version;
next();
}
app.get(/api/users, versionRouter, (req, res) => {
if (req.apiVersion === v1) return res.json({ users: [...] });
if (req.apiVersion === v2) return res.json({ data: { users: [...] }, meta: {} });
return res.status(400).json({ error: 不支持的版本: ${req.apiVersion} });
});
当未发送版本请求头时,始终定义回退行为——默认使用最新的稳定版本或返回 400 Bad Request。
API 的语义化版本控制
| SemVer 组件 | API 含义 | 需要采取的行动 |
|---|
| 主版本号 (v1 → v2) | 破坏性变更——删除字段、重命名端点、更改认证 | 客户端必须迁移 |
| 次版本号 (v1.1 → v1.2) |
新增、向后兼容——新的可选字段、新端点 | 无需客户端更改 |
|
修订号 (v1.1.0 → v1.1.1) | 错误修复,行为无变化 | 无需客户端更改 |
只有主版本号出现在 URL 路径中。通过变更日志传达次版本号和修订号。
破坏性变更与非破坏性变更
破坏性变更——需要新版本
| 变更 | 为何会破坏 |
|---|
| 删除响应字段 | 读取该字段的客户端得到 undefined |
| 重命名字段 |
从客户端角度来看等同于删除 |
| 更改字段类型 | id: 123 → id: 123 会破坏类型化客户端 |
| 删除端点 | 调用它的客户端得到 404 |
| 将可选参数变为必填 | 缺少该参数的现有请求开始失败 |
| 更改 URL 结构 | 已收藏/硬编码的 URL 失效 |
| 更改错误响应格式 | 客户端错误处理逻辑失效 |
| 更改认证机制 | 现有凭据停止工作 |
非破坏性变更——同一版本下安全
| 变更 | 为何安全 |
|---|
| 添加新的可选响应字段 | 客户端忽略未知字段 |
| 添加新端点 |
不影响现有端点 |
| 添加新的可选查询/请求体参数 | 现有请求无需它即可工作 |
| 添加新的枚举值 | 如果客户端优雅地处理未知值则安全 |
| 放宽验证约束 | 先前有效的请求仍然有效 |
| 提升性能 | 相同接口,更快响应 |
弃用策略
在没有警告的情况下切勿删除版本。遵循以下时间线:
阶段 1:宣布
• 响应中的 Sunset 请求头 • 变更日志条目
• 发送邮件/Webhook 给消费者 • 文档标记为已弃用
阶段 2:日落期
• v1 仍然工作但发出警告 • 监控 v1 流量
• 联系剩余消费者 • 提供迁移支持
阶段 3:移除
• v1 返回 410 Gone
• 响应体包含迁移指南 URL
• 将文档重定向到 v2
最短弃用期: 公共 API:12 个月 · 合作伙伴 API:6 个月 · 内部 API:1–3 个月
Sunset HTTP 请求头 (RFC 8594)
在来自已弃用版本的每个响应中包含:
HTTP/1.1 200 OK
Sunset: Sat, 01 Mar 2025 00:00:00 GMT
Deprecation: true
Link: ; rel=sunset
X-API-Warn: v1 已弃用。请在 2025-03-01 前迁移到 v2。
已退役版本响应
当超过日落期后,返回 410 Gone:
json
{
error: VersionRetired,
message: API v1 已于 2025-03-01 退役。,
migration_guide: https://api.example.com/docs/migrate-v1-v2,
current_version: v2
}
迁移模式
适配器模式
共享业务逻辑,特定版本的序列化:
python
class UserService:
async def getuser(self, userid: str) -> User:
return await self.repo.find(user_id)
def to_v1(user: User) -> dict:
return {id: user.id, name: user.full_name, email: user.email}
def to_v2(user: User) -> dict:
return {
id: user.id,
name: {first: user.firstname, last: user.lastname},
emails: [{address: e, primary: i == 0} for i, e in enumerate(user.emails)],
createdat: user.createdat.isoformat(),
}
外观模式
单一入口点委托给正确的版本化处理器:
python
async def getuser(userid: str, version: int):
user = await userservice.getuser(user_id)
serializers = {1: tov1, 2: tov2}
serialize = serializers.get(version)
if not serialize:
raise UnsupportedVersionError(version)
return serialize(user)
版本化控制器
每个版本有独立的控制器文件,共享服务层:
api/
v1/
users.py # v1 请求/响应结构
orders.py
v2/
users.py # v2 请求/响应结构
orders.py
services/
user_service.py # 与版本无关的业务逻辑
order_service.py
API 网关路由
在基础设施层路由版本:
yaml
routes:
- match: /api/v1/*
upstream: api-v1-service:8080
- match: /api/v2/*
upstream: api-v2-service:8080
多版本支持
架构:
请求 → API 网关 → 版本路由器 → v1 处理器 → 共享服务层 → 数据库
→ v2 处理器 ↗
原则:
- 1. 业务逻辑与版本无关。 服务、存储库和领域模型是共享的。
- 序列化是特定于版本的。 每个版本有自己的请求验证器和响应序列化器。
- 转换是显式的。 v1tov2 转换器记录了每个字段的映射。
- 测试覆盖所有活跃版本。 每个受支持的版本都有自己的集成测试套件。
最大并发版本数: 2–3 个活跃版本(当前版本 + 1–2 个已弃用版本)。超过 3 个会产生不可持续的维护负担。
客户端沟通
变更日志