REST API 版本控制策略完整教學:URI Path、Header、Query 三種方案實戰比較與最佳實踐
如果你曾經維護過一個上線的 API,你一定遇過這個兩難:新功能需要改變回應格式,但舊的客戶端還在用。硬改?App 直接崩潰。不改?技術債越堆越高。這就是為什麼API 版本控制是每個後端工程師遲早都要面對的課題。
在這篇文章中,我會把三大主流版本控制策略拆開來講,每種都附上程式碼範例跟實際運用場景,幫你在專案中做出最適合的選擇。如果你對 REST API 的整體架構還不太熟,建議先讀一下我們的 REST API 後端架構完整指南,會有更完整的脈絡。
為什麼 API 版本控制如此重要?
想像一下:你的 API 有三個不同版本的 mobile app 在串接,加上兩個第三方合作夥伴。某天產品經理說要把 user 回應裡的 name 欄位拆成 first_name 和 last_name。如果沒有版本控制機制,這個看似簡單的改動可能會影響所有客戶端。
API 版本控制的核心目標其實就三個:
- 穩定性:確保現有客戶端不會因為後端更新而壞掉
- 彈性:讓 API 能持續演進,不被舊設計綁死
- 溝通:讓 API 的變更對消費者來說是透明且可預期的
說白了,版本控制就是一份和你的 API 使用者之間的契約管理機制。而怎麼實作這份契約,就是接下來要討論的重點。
策略一:URI Path 版本控制
這大概是最常見、最直覺的做法。版本號直接寫在 URL 路徑裡:
GET /v1/users/123
GET /v2/users/123
看到網址就知道是哪個版本,連 Postman 測試都很方便。GitHub 的 API 就是用這種方式,api.github.com 搭配不同的路徑版本。
優點
- 最直覺,一看就懂
- 瀏覽器直接能測試,curl 也很方便
- API 文件容易組織,每個版本獨立成章
- CDN 和 API Gateway 的路由設定最簡單
缺點
- 嚴格來說違反了 REST 精神——同一個資源不該有兩個 URI
- 版本一多,程式碼容易出現大量重複
- 客戶端升版需要改所有 API 呼叫的網址
實務上,大多數團隊的第一選擇都是這個方案,因為學習成本最低,新人一看就懂。特別是你的 API 消費者不是很技術導向的時候(比如行銷部門要串接資料),URI Path 的直覺性完勝其他方案。
策略二:Custom Header 版本控制
這個做法是把版本資訊放在 HTTP Header 裡,常見的寫法有兩種:
# 自訂 Header
GET /users/123
X-API-Version: 2
# 或用 Accept Header(內容協商)
GET /users/123
Accept: application/vnd.myapi.v2+json
Stripe 就是用 Header 方式的經典案例。他們用自訂的 Stripe-Version Header,讓開發者可以精確控制要用哪個版本的 API 行為。
優點
- URL 保持乾淨,同一資源只有一個 URI
- 比較符合 REST 設計原則
- 可以做到很細粒度的版本控制(甚至到單一 endpoint 等級)
缺點
- 瀏覽器直接測試比較麻煩
- API 消費者需要額外設定 Header,門檻稍高
- Debug 的時候容易忽略 Header 設定,排錯成本增加
如果你的 API 主要是給內部團隊或技術能力較強的合作夥伴使用,Header 版本控制的優雅程度確實更高。但如果你的使用者裡有不少是剛入門的開發者,這個方案的學習門檻可能會成為痛點。
策略三:Query Parameter 版本控制
版本號放在查詢參數裡,像這樣:
GET /users/123?version=2
GET /users/123?v=2
Google 的不少 API 都採用這種方式,通常搭配一個預設版本,沒帶參數就使用最新(或最穩定)的版本。
優點
- 不影響 URL 結構,資源路徑維持一致
- 可以設定預設版本,呼叫端不帶參數也能正常運作
- 實作簡單,路由不需要大改
缺點
- 容易跟其他查詢參數搞混
- 快取策略需要特別注意(CDN 可能忽略 query string)
- 在 API 文件中的呈現不夠直覺
老實說,我個人不太推薦這個方式當作主要的版本控制策略。它更適合作為輔助手段,比如用來切換某些實驗性功能的行為。
三種方案完整比較
| 比較面向 | URI Path | Custom Header | Query Param |
|---|---|---|---|
| 直覺程度 | ★★★★★ | ★★★ | ★★★★ |
| REST 合規性 | ★★ | ★★★★★ | ★★★ |
| 快取友善度 | ★★★★★ | ★★★ | ★★ |
| 瀏覽器測試 | ★★★★★ | ★★ | ★★★★ |
| 路由複雜度 | 中等 | 較高 | 低 |
| 適合場景 | 公開 API、多數場景 | 內部 API、進階用戶 | 輔助機制、實驗功能 |
如果你正在建構一個給外部開發者使用的公開 API,我的建議是從 URI Path 開始。等團隊跟產品都更成熟了,再考慮要不要遷移到 Header-based 的方案。想了解更多 API 設計考量,也可以參考 GraphQL vs REST API 完整比較這篇文章。
知名企業怎麼選?實戰案例分析
GitHub — URI Path
GitHub API 使用 URI Path 搭配 Accept Header 作為輔助。預設走最新版,但開發者可以透過 Header 指定版本。這種混合策略兼顧了易用性和彈性。
Stripe — Custom Header
Stripe 的做法很有意思:每個 API Key 都綁定一個版本,除非你在 Request 的 Header 裡明確指定。這代表你可以在不改程式碼的情況下,透過 Dashboard 升版。這對支付相關的 API 來說特別重要,因為任何破壞性變更都可能造成金錢損失。
Google Cloud — Query Parameter
Google 的 API 常用 query parameter,但他們也大量使用 API Discovery 機制,讓客戶端 SDK 自動處理版本邏輯,開發者其實不太需要自己管版本。
向後相容性與棄用策略
選好版本控制方式只是第一步,更重要的是建立一套棄用(deprecation)流程:
- 提前通知:至少在棄用前 6 個月發出公告,透過 Email、API 回應的 Header(如
SunsetHeader),以及文件更新 - 漸進式遷移:新舊版本並行至少 3-6 個月,給消費者足夠的遷移時間
- 監控使用量:追蹤舊版本的呼叫量,當流量低於某個閾值才真正關閉
- 回應提示:在舊版本的回應中加入
WarningHeader 或deprecation_notice欄位
向後相容性的基本原則是「只加不改不刪」:新增欄位沒問題,修改或刪除現有欄位就需要新版本。這點在設計 API Rate Limiting 的時候也一樣重要,限流規則的變更同樣需要版本管理。
實作指南:Node.js + Express 範例
來看看在 Express 中怎麼實作 URI Path 版本控制:
// routes/v1/users.js
const express = require('express');
const router = express.Router();
router.get('/users/:id', async (req, res) => {
const user = await User.findById(req.params.id);
res.json({
id: user.id,
name: user.name, // v1: 完整姓名
email: user.email
});
});
module.exports = router;
// routes/v2/users.js
const express = require('express');
const router = express.Router();
router.get('/users/:id', async (req, res) => {
const user = await User.findById(req.params.id);
res.json({
id: user.id,
first_name: user.firstName, // v2: 拆分姓名
last_name: user.lastName,
email: user.email,
created_at: user.createdAt // v2: 新增欄位
});
});
module.exports = router;
// app.js
const app = express();
app.use('/v1', require('./routes/v1/users'));
app.use('/v2', require('./routes/v2/users'));
Header-based 版本控制的 Middleware 則可以這樣寫:
const versionMiddleware = (req, res, next) => {
const version = req.headers['x-api-version'] || '1';
req.apiVersion = parseInt(version, 10);
next();
};
app.use(versionMiddleware);
app.get('/users/:id', async (req, res) => {
const user = await User.findById(req.params.id);
if (req.apiVersion >= 2) {
return res.json({
id: user.id,
first_name: user.firstName,
last_name: user.lastName
});
}
res.json({ id: user.id, name: user.name });
});
這兩種實作方式各有取捨。URI Path 的程式碼分離比較乾淨,但重複性高;Header-based 的邏輯集中,但 conditional 一多就會變得難維護。在實際專案中,很多團隊會搭配 JWT 認證機制 一起設計版本路由,把認證和版本控制整合在同一層 middleware 裡。
最佳實踐總結
做了這麼多年 API 開發,以下是我整理出來最實用的幾條建議:
- 不要過早版本化:如果你的 API 還在快速迭代,先把精力放在設計好的資料模型上,而不是急著建版本機制
- 主版本號就夠了:v1、v2、v3,不需要 v1.2.3 這種細到小數點的版本。語義化版本控制留給 Library,不是 API
- 文件先行:每次版本更新都要同步更新 API 文件,包含 changelog 和遷移指南
- 自動化測試:每個版本都要有對應的整合測試,CI/CD 跑的時候全部版本都要通過
- 設定日落期限:每個版本都應該有明確的 End-of-Life 日期,寫在文件裡,也寫在 HTTP Header 裡
- 統一團隊共識:版本策略是架構決策,不是個人偏好。在 ADR(Architecture Decision Record)中記錄為什麼選擇某個方案
最後提醒一下,版本控制只是 API 生命週期管理的一環。真正好的 API 設計,是讓版本控制的需求降到最低——透過良好的資料建模、寬鬆的輸入驗證(Postel's Law),和充足的擴展性預留。回到我們的 REST API 架構指南,從設計階段就把這些考量納入,才是長遠之道。
繼續閱讀
WebSocket 即時通訊實戰:用 Node.js 從零打造聊天室完整教學
WebSocket 讓瀏覽器與伺服器能建立持久的雙向連線,本文用 Node.js ws 套件實作完整的聊天室系統。
相關文章
你可能也喜歡
探索其他領域的精選好文