API 版本策略 是做什麼的?
TL;DR: 基於日期、按 API 金鑰固定的版本(Stripe 模型)比
/v1/、/v2/更適合長期存在的公共 API,因為它允許進行小規模的重大變更,而無需強制客戶端遷移到新的版本樹。對於內部 API,僅添加的演進加上Deprecation和Sunset標頭(RFC 9745, RFC 8594)通常就足夠了。務必提前公告,務必在指定日期廢棄,切勿直接刪除。
快速導覽
| 狀況 | 章節 |
|---|---|
| "需要中斷一個欄位,但有 10 萬個 API 金鑰" | 基於日期固定的版本 |
"我該用 /v1/ 還是 Accept: application/vnd.foo.v2+json?" | 策略比較 |
| "如何告訴客戶端某個端點將被棄用?" | Deprecation + Sunset |
| "在程式碼中維護 6 個版本快把我們搞垮了" | 版本轉換器 |
| "內部 API —— 我們真的需要版本控制嗎?" | 內部 API 與公共 API |
決策圖
flowchart TD
A[需要改變 API 行為] --> B{僅添加?<br/>新欄位、新端點、新的可選參數}
B -->|是| C[直接發布<br/>無需版本控制]
B -->|否,屬於中斷性變更| D{API 是公共的<br/>+ 客戶端較多且不透明?}
D -->|否,內部/客戶端較少| E[協調遷移<br/>+ Deprecation/Sunset 標頭]
D -->|是,公共| F{能否為每個請求<br/>構建兼容層?}
F -->|可以,有轉換器| G[基於日期固定的版本<br/>Stripe 模型]
F -->|不行,需要大規模重寫| H[主要版本 URI 提升<br/>/v1 → /v2]
E --> I[設定 Deprecation: @timestamp<br/>+ Sunset: HTTP-date<br/>+ Link: rel=deprecation]
G --> I
H --> I
1. 基於日期固定的版本(Stripe 模型)
Stripe 在 API versioning at Stripe 中發布了其架構:
用戶第一次發出 API 請求時,其帳戶會自動固定到最新的可用版本,之後該用戶進行的每個 API 呼叫都會隱式地分配到該版本。
[版本是] 以發布日期命名的滾動版本(例如
2017-05-24)。
關鍵屬性:
| 屬性 | 為什麼有效 |
|---|---|
| 每個帳戶的默認版本(在首次調用時設定) | 新客戶自動固定到最新版本;既有客戶在您發布變更時不會中斷 |
Stripe-Version 標頭可覆蓋每個請求的固定版本 | 允許逐步遷移客戶端:SDK 可以在帳戶升級前,先選擇使用新版本 |
基於日期的命名(2024-04-10) | 傳達時效性;無需爭論 "v3" 的含義;允許任意多次的小規模中斷,而非累積到一次大規模的 v3 發布 |
| 儀表板升級路徑 | 客戶可以預覽差異,然後升級其帳戶的默認版本 |
這是唯一能擴展到真正長期存在的公共 API 的方法(Stripe 的版本可追溯到十年前)。它會增加您的內部複雜度(第 4 節中的轉換器模式),但能讓您的客戶免於永無止境的 /v1/ → /v2/ 遷移週期。
2. 版本策略比較
| 策略 | 版本位置 | 優點 | 缺點 | 最佳適用場景 |
|---|---|---|---|---|
URI 區段(/v1/, /v2/) | 路徑 | 快取友好(不同 URL = 不同快取條目);無需客戶端配置 | 無法進行小規模中斷;強制進行版本樹分支;URL 不再是 "穩定的資源標識符"(根據 Fielding 的說法) | 內部 API,很少中斷的公共 API |
自定義標頭(Stripe-Version: 2024-04-10) | 請求標頭 | 大量版本開銷低;每個請求可細粒度控制 | 無法共享內嵌版本的 URL;在瀏覽器網址列中測試較困難 | 大規模公共 API |
Accept 媒體類型(Accept: application/vnd.foo.v2+json) | 標準 Accept 標頭 | 符合 HTTP "規範";重用內容協商機制 | 設定麻煩;工具支援不一;透過 curl 進行除錯的指令冗長 | 超媒體 API,深度 RESTful 設計 |
查詢參數(?version=2) | URL | 易於測試;在日誌中可見 | 污染 URL;語義上不正確(版本不是資源屬性) | 僅限快速原型開發 |
| 無版本控制,僅添加 | 無 | 零開銷;無需客戶端協調 | 若不建立新端點,則永遠無法進行中斷性變更 | 具有共同部署的內部微服務 |
根據您的限制條件選擇:
- "我們頻繁進行小規模中斷" → 基於日期的標頭(Stripe)
- "我們每 5 年進行一次大規模重寫" → URI 區段(
/v1/,/v2/) - "我們從不中斷,只添加" → 無版本控制 + 僅添加演進
3. Deprecation/Sunset 標頭對
兩個 IETF RFC 涵蓋了生命週期信號:
Sunset 標頭 — RFC 8594
Sunset 的值是一個 HTTP-date 時間戳,如 [RFC7231] 第 7.1.1.1 節所定義,並且 SHOULD 是未來的時間戳。
Sunset: Sat, 31 Dec 2026 23:59:59 GMT
表示 "該資源預計在未來某個特定時間點變得無響應"。客戶端 SHOULD 將該時間戳視為提示,而非硬性合約。
Deprecation 標頭 — RFC 9745
Deprecation: @1735689599
Sunset: Sun, 31 Dec 2026 23:59:59 GMT
Link: <https://api.example.com/docs/migrate-v1-to-v2>; rel="deprecation"
Deprecation 的值是一個 Unix 時間戳(秒,根據 Structured Fields 規範前綴 @)。它可以是過去的("已棄用")或未來的("將要棄用")。必須遵守的事項:
- MUST 使用 RFC 9651 中的結構化欄位日期格式
SunsetMUST NOT 早於Deprecation——規範非常明確;這是一個時間順序約束- SHOULD 包含一個
Link,其rel="deprecation"指向遷移文件
發送 Deprecation 的行為並不會改變資源的行為——它是一個信號,而非降級。伺服器 SHOULD 在 Sunset 日期之前保持資源的功能正常(緊急情況除外)。
建議的時間線
T+0 首次在生產環境發送 Deprecation 標頭
T+30d 公開公告(更新日誌、電子郵件、文件)
T+90d 在回應日誌/指標中添加對已棄用路徑使用情況的記錄
T+180d 設定 Sunset 日期,提前 90 天
T+270d Sunset 日期到來——端點返回 410 Gone 並附帶遷移連結
對於主要版本固定的 API 上的小規模中斷性變更:中位數為 6 個月。對於完整的 v1 → v2 廢棄:12-24 個月。
4. 版本轉換器模式
Stripe 的內部架構解決了 "我們在生產環境中有 50 個版本" 的問題:
[該系統使用] API 資源類別來定義當前的 API 回應結構,並結合版本變更模組來封裝向後不相容的轉換。在處理回應時,系統向後回溯時間,並套用沿途找到的每個版本變更模組,直到達到目標版本。
其形式:
// resource: 規範(最新)表示
const charge = {
id: 'ch_123',
amount: 1000,
payment_method_details: { card: { brand: 'visa', last4: '4242' } },
}
// 每個中斷性變更都是一個轉換器模組:
const v_2024_03_01 = {
// 移除 payment_method_details 下的 'card' 巢狀,展平欄位
apply(resource) {
return {
...resource,
card_brand: resource.payment_method_details?.card?.brand,
card_last4: resource.payment_method_details?.card?.last4,
}
},
}
const v_2023_10_15 = {
// 將 'amount' 重新命名為 'amount_cents'
apply(resource) {
const { amount, ...rest } = resource
return { ...rest, amount_cents: amount }
},
}
// 管線按時間倒序套用轉換器,直到達到請求的版本
function transform(resource, requestedVersion) {
const chain = transformers.filter(t => t.date > requestedVersion)
return chain.reduceRight((r, t) => t.apply(r), resource)
}
優勢:
- 核心程式碼保持現代化——每個端點都針對最新的 schema 編寫;業務邏輯中不會散落
if (version < ...)。 - 每個中斷性變更都是一個檔案——易於審查、易於隔離測試、易於在最後一個用戶固定版本越過該變更後刪除。
- 遙測——您可以統計每天觸發哪些轉換器,以便根據實際使用情況規劃廢棄。
代價:構建轉換器框架。如果您每年有 >3 次中斷性變更且 >10k 個 API 消費者,那麼這值得投入;對於 <50 個客戶的內部 API 則過於複雜。
5. 內部 API 與公共 API
其不對稱性很重要:
| 內部 | 公共 | |
|---|---|---|
| 是否可以協調部署? | 可以——原子交換 | 不行——客戶端獨立部署 |
| Schema 演進 | 僅添加 + Slack 訊息 | 嚴格的版本控制 + 電子郵件 + 儀表板 + 部落格文章 |
| 廢棄窗口 | 數天到數週 | 6-24 個月 |
| 版本控制需求 | 通常不需要——只需 Deprecation 標頭 | 強制 |
| 最佳策略 | 僅添加演進;僅在重寫時使用 URI 版本 | 基於日期的標頭(如果規模允許),主要重寫使用 URI |
陷阱:公司將所有 API 都視為 "公共" API,並為僅由 3 個內部服務使用的 API 構建了笨重的版本控制基礎設施。相反的陷阱:一個洩漏的內部 API 有了外部消費者,而您實際上無法中斷它。
防禦措施:將 "公共" 作為服務上的一個二進位標籤。公共服務獲得完整的版本控制儀式;內部服務則沒有,但會對誰在呼叫它們進行積極監控。
反模式
| 反模式 | 為什麼會出問題 | 修正方案 |
|---|---|---|
為了單一中斷性變更將 /v1/ 提升到 /v2/ | 強制客戶端為了一個欄位遷移所有東西 | 基於日期的版本或僅添加演進 |
移除端點時沒有 Sunset 標頭 | 客戶端在沒有任何警告的情況下中斷;支援工單爆炸 | 務必在移除前 90 天以上配對 Deprecation 標頭 |
發送了 Deprecation 但沒有 Link | 客戶端知道它已棄用,但不知道該怎麼辦 | 務必包含 Link: <migration-doc>; rel="deprecation" |
| 為每個客戶硬編碼版本例外 | 變得無法維護;無法推斷行為 | 使用轉換器模式;"例外" 變為版本固定 |
| 像對待公共 API 一樣對內部 API 進行版本控制 | 流程開銷大但沒有回報 | 僅添加演進 + 原子部署 + Slack |
在 REST API 上使用 semver(v1.2.3) | 暗示了不適用的含義(對於 JSON schema 來說什麼是 "修補"?) | 基於日期的命名或僅主版本(v1) |
| 設定了 Sunset 日期但未強制執行 | 端點永遠存在,程式碼腐爛 | 日曆提醒;在 Sunset 前 7 天失敗的 CI 測試以強制行動 |
新手 / 專家 / 時間線
| 新手 | 專家 | |
|---|---|---|
| 第一次中斷性變更 | 將 /v1 提升到 /v2,複製貼上整個版本樹 | 考慮基於日期的固定,評估變更是否確實是中斷性的 |
| 告知客戶端有關棄用的訊息 | 發送大量電子郵件 | Deprecation + Sunset 標頭 + Link + 電子郵件 + 儀表板橫幅 |
| 維護 5 個版本 | 在每個控制器中使用 5 個 if/else 分支 | 轉換器模組,僅保留最新版本的核心程式碼 |
| 強制執行 Sunset | "我們會處理的" | 在 CI 中設定 Sunset 日期;端點在該日期變為 410 Gone |
| 內部 API 變更 | 與公共 API 相同的儀式——緩慢 | 僅添加演進 + 原子部署;為公共 API 保留繁重流程 |
時間線測試:從 "我們想要移除欄位 X" 到 "欄位 X 在生產環境中消失" 需要多長時間?一個專家團隊會有一個可重複的答案(例如,90 天)。一個新手團隊則沒有答案,因為沒有流程。
品質關卡
當滿足以下條件時,版本控制變更才能發布:
- 測試: 移除或重新命名任何外部可見的欄位,必須以至少 N 天前發布的棄用公告為前提(其中 N 是記錄在案的廢棄窗口)。
- 測試: 每個已棄用的端點都發送
Deprecation: @<unix-ts>和Sunset: <HTTP-date>標頭,並在整合測試中驗證。 - 測試:
Sunset值永遠不早於Deprecation值(根據 RFC 9745 的 MUST 要求)。 - 測試: 每個棄用公告都包含一個
Link: <docs-url>; rel="deprecation"標頭,並且該 URL 存在一個公共的遷移文件(在 CI 中進行 HEAD 檢查)。 - 測試: 存在每個版本的使用情況遙測——在刪除 v2 之前,您可以回答 "昨天 v2 路徑上有多少請求?"。
- 測試(僅限日期固定): 新的 API 金鑰預設使用最新版本;既有的金鑰在部署期間保持其固定版本。
- 手動: Sunset 強制執行計劃:一個日曆提醒、一個 CI 檢查或一個自動化的 410 Gone——總有某個機制 在日期到來時觸發。
不屬於本技能的內容
- GraphQL schema 演進(請使用
graphql-schema-evolution) - gRPC / Protobuf 線路兼容性(請使用
protobuf-evolution-rules) - 資料庫 schema 版本控制(請使用
zero-downtime-database-migration) - 函式庫 / SDK semver(請使用
npm-package-versioning) - 事件流 schema 演進(請使用
kafka-schema-registry-design)
資料來源
- Stripe: API versioning at Stripe — 固定的基於日期的版本、轉換器模式
- Stripe API: Idempotent requests / versioning policy
- IETF: RFC 8594 — The Sunset HTTP Header Field
- IETF: RFC 9745 — The Deprecation HTTP Header Field
- IETF: RFC 9651 — Structured Field Values for HTTP(Deprecation 標頭的日期格式)
- Roy Fielding: REST APIs must be hypertext-driven — 反對 URI 版本控制的經典論證