跳至主要內容
Skip to content

API 版本控制策略:演進與相容

API 會隨著業務需求演進。良好的版本控制策略能讓 API 平滑升級,同時不破壞現有客戶端。


一、 為什麼需要版本控制?

1.1 破壞性變更

這些變更會破壞現有客戶端:

  • 移除欄位或端點
  • 更改欄位類型
  • 更改必填欄位
  • 更改認證方式
  • 更改回應結構

1.2 版本控制的目標


二、 版本控制方式

2.1 URL 路徑

bash
# 最常見的方式
GET /v1/users
GET /v2/users

# 或使用日期
GET /2024-01-01/users

優點

  • 簡單直觀
  • 容易路由
  • 可以快取

缺點

  • URL 變動
  • 可能被認為「不 RESTful」

2.2 查詢參數

bash
GET /users?version=1
GET /users?version=2

優點

  • URL 保持穩定

缺點

  • 容易被忽略
  • 可能與其他參數混淆

2.3 請求標頭

bash
GET /users
Accept: application/vnd.myapi.v1+json

# 或自定義標頭
GET /users
X-API-Version: 1

優點

  • URL 完全穩定
  • 更「純粹」的 REST

缺點

  • 不易測試(需要設定標頭)
  • 不能直接在瀏覽器測試

2.4 內容協商

bash
GET /users
Accept: application/vnd.myapi+json; version=2

2.5 比較表

方式URL 穩定易用性可快取推薦度
URL 路徑⭐⭐⭐⭐⭐⭐推薦
查詢參數⭐⭐⭐⭐中等
請求標頭⭐⭐進階

三、 Express 實作

3.1 URL 路徑版本

javascript
// routes/v1/users.js
const router = require("express").Router();

router.get("/", (req, res) => {
  res.json({
    users: [{ id: 1, name: "John", email: "john@example.com" }],
  });
});

module.exports = router;

// routes/v2/users.js
const router = require("express").Router();

router.get("/", (req, res) => {
  res.json({
    data: [
      {
        id: 1,
        fullName: "John Doe", // 欄位名稱變更
        contact: { email: "john@example.com" }, // 結構變更
      },
    ],
    pagination: { page: 1, total: 100 },
  });
});

module.exports = router;

// app.js
app.use("/v1/users", require("./routes/v1/users"));
app.use("/v2/users", require("./routes/v2/users"));

3.2 標頭版本

javascript
function versionMiddleware(req, res, next) {
  const version = req.headers["x-api-version"] || "1";
  req.apiVersion = parseInt(version);
  next();
}

app.use(versionMiddleware);

app.get("/users", (req, res) => {
  if (req.apiVersion >= 2) {
    return res.json({ data: usersV2 });
  }
  res.json({ users: usersV1 });
});

3.3 版本控制器

javascript
class VersionedController {
  constructor() {
    this.handlers = new Map();
  }

  version(v, handler) {
    this.handlers.set(v, handler);
    return this;
  }

  handle(req, res, next) {
    const version = req.apiVersion || 1;

    // 找到匹配或更低的版本
    let handler = null;
    for (let v = version; v >= 1; v--) {
      if (this.handlers.has(v)) {
        handler = this.handlers.get(v);
        break;
      }
    }

    if (handler) {
      return handler(req, res, next);
    }

    res.status(400).json({ error: "Unsupported API version" });
  }
}

// 使用
const getUsers = new VersionedController()
  .version(1, (req, res) => res.json({ users: [] }))
  .version(2, (req, res) => res.json({ data: [], pagination: {} }));

app.get("/users", (req, res, next) => getUsers.handle(req, res, next));

四、 廢棄策略

4.1 標頭通知

javascript
function deprecationMiddleware(version, sunsetDate) {
  return (req, res, next) => {
    res.set("Deprecation", "true");
    res.set("Sunset", sunsetDate);
    res.set("Link", '</v2/users>; rel="successor-version"');

    console.warn(`Deprecated API v${version} called: ${req.path}`);

    next();
  };
}

app.use("/v1", deprecationMiddleware(1, "Sat, 01 Jan 2025 00:00:00 GMT"));

4.2 回應中提示

javascript
app.get("/v1/users", (req, res) => {
  res.json({
    _deprecated: true,
    _deprecationMessage:
      "This endpoint will be removed on 2025-01-01. Please migrate to /v2/users",
    _successor: "/v2/users",
    users: [],
  });
});

4.3 廢棄時程


五、 語義化版本

5.1 SemVer 原則

MAJOR.MINOR.PATCH

MAJOR: 破壞性變更
MINOR: 向後兼容的新功能
PATCH: 向後兼容的修復

5.2 API 版本 vs 實作版本

javascript
// API 版本:v1, v2(主要版本)
// 實作版本:1.2.3(可以頻繁更新)

// 標頭中可以同時提供
res.set("X-API-Version", "2");
res.set("X-Implementation-Version", "2.15.3");

六、 向後兼容技巧

6.1 添加欄位(安全)

javascript
// v1.0
{ "id": 1, "name": "John" }

// v1.1(向後兼容)
{ "id": 1, "name": "John", "email": "john@example.com" }

6.2 欄位重命名

javascript
// 同時保留兩個欄位
{
  "name": "John",           // 舊欄位(廢棄)
  "fullName": "John Doe"    // 新欄位
}

6.3 結構變更

javascript
// 使用轉換層
function transformToV1(v2Data) {
  return {
    users: v2Data.data.map((user) => ({
      id: user.id,
      name: user.fullName,
      email: user.contact.email,
    })),
  };
}

function transformToV2(v1Data) {
  return {
    data: v1Data.users.map((user) => ({
      id: user.id,
      fullName: user.name,
      contact: { email: user.email },
    })),
    pagination: { page: 1, total: v1Data.users.length },
  };
}

七、 文檔與通知

7.1 變更日誌

markdown
# API Changelog

## v2.0.0 (2025-01-01)

### Breaking Changes

- `GET /users` response structure changed
- `name` field renamed to `fullName`
- `email` moved to `contact.email`

### Migration Guide

1. Update response parsing
2. Use `fullName` instead of `name`
3. Access email via `contact.email`

## v1.5.0 (2024-12-01)

### Added

- `GET /users/:id/orders` endpoint
- `phone` field in user response

### Deprecated

- `GET /v1/users` (use v2)

7.2 通知客戶端

javascript
// 電子郵件通知
// Slack/Discord webhook
// 儀表板公告

// API 內提示
app.use((req, res, next) => {
  if (majorVersionAnnouncement) {
    res.set("X-API-Announcement", "Version 3 coming soon! Check docs.");
  }
  next();
});

總結

策略適用場景
URL 路徑大多數 API
請求標頭企業級 API
查詢參數簡單 API
無版本內部 API

> **最佳實踐**:

  • 從第一天就有版本
  • 保持至少一個舊版本
  • 給予足夠的遷移時間
  • 清晰的廢棄通知

進階挑戰

  1. 設計一個版本遷移工具,自動轉換請求/回應格式。
  2. 研究 Stripe API 的版本策略,分析其優缺點。
  3. 實作一個 API 版本監控系統,追蹤各版本使用量。

延伸閱讀與資源