為什麼要版本化

API 公開出去之後就有消費者(前端、mobile app、第三方整合)在用。你不可能強迫所有消費者同步更新——mobile app 的升級率從來不是 100%,第三方整合商可能根本不知道你改了。

所以當你要改一個已公開的 API,你有兩個選擇:

  1. 不 breaking change:新增欄位、新增 endpoint、把 deprecated 欄位保留但不再建議使用——這些不需要新版本
  2. 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) { /* ... */ }
  }
}

延伸閱讀