DevToolBox免费
博客

API 版本控制策略:URL、Header 与内容协商

12 分钟作者 DevToolBox

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

版本控制策略对比

StrategyProsConsExampleBest For
URL Path (/v2/)Explicit, cacheable, bookmarkable, easy to testVersion in URL feels "dirty" to REST purists/api/v2/usersPublic APIs, open source
Custom HeaderClean URLs, explicit versioningLess visible, harder to test in browsersAPI-Version: 2Enterprise internal APIs
Accept HeaderHTTP standard, content negotiationComplex to implement and testAccept: vnd.api.v2+jsonRESTful purists
Query ParameterSimple, no path changesBreaks REST semantics, bad for caching/api/users?v=2Internal tools, prototypes
No versioning (GraphQL)Single endpoint, schema evolutionComplex schema managementPOST /graphqlGraphQL APIs

最佳实践

  1. 绝不在没有至少 6 个月迁移窗口(企业 API 12 个月以上)的情况下破坏现有版本化 API。
  2. 使用 Sunset 请求头以编程方式向 API 客户端传达生命周期结束日期。
  3. 为每个 API 版本维护变更日志,记录更改内容和原因。
  4. 考虑语义版本控制:只针对破坏性更改递增主版本。次要版本可以添加新端点而不递增 URL 版本。
  5. 发布新主版本时,提供包含代码示例的迁移指南。

常见问题

哪种 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 上并行运行模式。

相关工具

𝕏 Twitterin LinkedIn
这篇文章有帮助吗?

保持更新

获取每周开发技巧和新工具通知。

无垃圾邮件,随时退订。

试试这些相关工具

📡HTTP Request Builder{ }JSON Formatter🔓CORS Tester

相关文章

GraphQL vs REST API:2026 年该用哪个?

深入比较 GraphQL 和 REST API,附代码示例。学习架构差异、数据获取模式、缓存策略,以及何时选择哪种方案。

API 限流指南:策略、算法与实现

API 限流完整指南。学习令牌桶、滑动窗口、漏桶算法及代码示例。包含 Express.js 中间件、Redis 分布式限流和最佳实践。

Nginx 反向代理配置:负载均衡、SSL 与缓存

Nginx 反向代理完整配置:上游服务器、负载均衡策略、SSL 证书终止、缓存与限流生产部署。