為什麼要版本化
API 公開出去之後就有消費者(前端、mobile app、第三方整合)在用。你不可能強迫所有消費者同步更新——mobile app 的升級率從來不是 100%,第三方整合商可能根本不知道你改了。
所以當你要改一個已公開的 API,你有兩個選擇:
- 不 breaking change:新增欄位、新增 endpoint、把 deprecated 欄位保留但不再建議使用——這些不需要新版本
- Breaking change:刪除欄位、改欄位型別、改 response 結構——這需要新版本,讓舊消費者繼續用舊的,新消費者用新的
什麼算是 breaking change:
Breaking(需要新版本):
- 刪除欄位或 endpoint
- 改欄位型別(string → number)
- 改 response 結構(data.user → data)
- 改欄位名稱(user_name → username)
- 改 HTTP method(POST → PUT)
- 改 HTTP status code 語意
Non-breaking(不需要新版本):
- 新增欄位(消費者應該忽略不認識的欄位)
- 新增 endpoint
- 把 required 欄位改成 optional
- 放寬 validation 規則(min length 10 → 5)
三種版本化策略
URL Path Versioning(最常見)
GET /v1/users
GET /v2/users
POST /v1/orders
// Express:route group 分版本
const v1Router = express.Router();
const v2Router = express.Router();
v1Router.get('/users', v1UserController.findAll);
v2Router.get('/users', v2UserController.findAll); // v2 的 response 結構不同
app.use('/v1', v1Router);
app.use('/v2', v2Router);優點:直觀、容易 debug(log 裡直接看到版本)、可以用不同 server 或 deployment 跑不同版本。
缺點:URL 不 RESTful 純粹主義者不喜歡(/users 是資源,版本不是資源的屬性);版本多了 route 管理變複雜。
適合:大部分場景,特別是有外部消費者的公開 API。
Header Versioning
GET /users
Accept: application/vnd.myapi.v2+json
或
GET /users
API-Version: 2
// Middleware 讀 header 決定版本
const versionMiddleware = (req, res, next) => {
const version = req.headers['api-version'] || '1';
req.apiVersion = parseInt(version);
next();
};
router.get('/users', versionMiddleware, async (req, res) => {
if (req.apiVersion >= 2) {
return res.json(await v2UserService.findAll());
}
return res.json(await v1UserService.findAll());
});優點:URL 乾淨、符合 REST 純粹主義(同一個資源同一個 URL)。
缺點:難 debug(瀏覽器 URL bar 看不出版本);curl / Postman 測試要記得帶 header;難做 CDN cache(cache key 要包 header)。
適合:內部 API、消費者都是可控的系統(不是第三方或 mobile app)。
Query Parameter Versioning
GET /users?version=2
GET /users?v=2
優點:簡單。
缺點:query param 容易被遺忘、被 CDN strip、跟其他 query param 混在一起。幾乎沒有公開 API 推薦這個方式。
適合:幾乎不推薦——除非是臨時性的降級機制(?force_v1=true)。
版本的生命週期
v1 發布 → v2 發布(v1 進入 deprecated)→ v1 sunset(停止服務)
Sunset 通知:
// Deprecated version 的 response 加 header
if (req.apiVersion === 1) {
res.set('Sunset', 'Sat, 31 Dec 2026 23:59:59 GMT');
res.set('Deprecation', 'true');
res.set('Link', '</v2/users>; rel="successor-version"');
}Sunset 策略建議:
- 公開 API(有外部消費者):deprecated 後至少 12 個月才 sunset
- 內部 API(只有自己的系統在用):deprecate 後 2–3 個 sprint 就可以 sunset,但要先確認所有消費者已遷移
版本化的實際操作建議
不要過早版本化:v1 應該在你真的需要 breaking change 的時候才出現,不是一開始就建 v1 的習慣。很多內部 API 永遠不需要版本化。
同時維護的版本不要超過 2 個:v1 + v2 是可以的;v1 + v2 + v3 是維護噩夢。規則是:發 v3 前先 sunset v1。
版本化 route,不是 service:
// ✅ Service 層不感知版本,router 層做轉換
v1Router.get('/users/:id', async (req, res) => {
const user = await userService.findById(req.params.id);
// v1 格式:扁平結構
res.json({ id: user.id, name: user.name, email: user.email });
});
v2Router.get('/users/:id', async (req, res) => {
const user = await userService.findById(req.params.id);
// v2 格式:巢狀結構
res.json({ data: user, meta: { version: 2 } });
});
// ❌ Service 裡加 version 判斷
class UserService {
async findById(id: string, version: number) {
if (version === 1) { /* ... */ }
else if (version === 2) { /* ... */ }
}
}