本文 AI 產出,尚未審核

ExpressJS (TypeScript) – Swagger / OpenAPI 文件化

主題:建立 API 檔案版本管理


簡介

在現代的 Web 開發中,API 版本管理 是不可或缺的基礎設施。隨著產品功能不斷演進,舊的介面仍需對外提供支援,而新功能則必須以全新版本上線。若沒有明確的版本機制,開發團隊很容易在升級時破壞既有客戶端,導致服務中斷與信任流失。

將版本資訊與 Swagger / OpenAPI 文件結合,不僅可以自動產生清晰的 API 文檔,還能讓前後端、測試與第三方開發者即時了解每個版本的差異與支援範圍。本文將以 ExpressJS + TypeScript 為例,說明如何在專案中落實 API 檔案版本管理,並搭配 swagger-ui-expressswagger-jsdoc 產出多版本的 OpenAPI 規格。


核心概念

1. 為什麼要在路由層面寫入版本號?

  • 向後相容:舊版客戶端仍能呼叫 v1 的介面,新的功能則放在 v2v3 中。
  • 清晰的升級路徑:開發者可以在文件中看到「此端點已在 v2 中棄用」的提示。
  • 獨立部署:不同版本的路由可以分別掛載中間件、驗證或限流策略。

2. OpenAPI 中的 serverspaths 如何配合版本?

OpenAPI 規範允許在 servers 中定義 base URL,例如:

servers:
  - url: https://api.example.com/v1
    description: API 第 1 版
  - url: https://api.example.com/v2
    description: API 第 2 版

paths 中,不需要重複寫入版本號,因為 servers 已經決定了 base path。這樣的設計可以讓同一套 Swagger 定義同時支援多個版本,只要在 UI 中切換 servers 即可。

3. 建立版本化的目錄結構

src/
├─ api/
│  ├─ v1/
│  │   ├─ user.controller.ts
│  │   └─ user.routes.ts
│  ├─ v2/
│  │   ├─ user.controller.ts
│  │   └─ user.routes.ts
│  └─ swagger/
│      ├─ v1.swagger.ts
│      └─ v2.swagger.ts
├─ app.ts
└─ server.ts

好處:每個版本的控制器、路由與 Swagger 定義皆獨立,互不干擾,後續維護與測試更為方便。

4. 使用 swagger-jsdoc 產生多版本文件

swagger-jsdoc 會根據 JSDoc 註解產出 JSON。只要給予不同的 definitionapis 路徑,即可產出多套規格。

// src/swagger/v1.swagger.ts
import swaggerJSDoc from 'swagger-jsdoc';

const options = {
  definition: {
    openapi: '3.0.0',
    info: {
      title: 'My API - v1',
      version: '1.0.0',
      description: '第一版 API 介面',
    },
    servers: [{ url: '/api/v1', description: 'v1 server' }],
  },
  // 只掃描 v1 目錄下的 *.ts
  apis: ['./src/api/v1/**/*.ts'],
};

export const swaggerSpecV1 = swaggerJSDoc(options);
// src/swagger/v2.swagger.ts
import swaggerJSDoc from 'swagger-jsdoc';

const options = {
  definition: {
    openapi: '3.0.0',
    info: {
      title: 'My API - v2',
      version: '2.0.0',
      description: '第二版 API,加入新功能與欄位',
    },
    servers: [{ url: '/api/v2', description: 'v2 server' }],
  },
  apis: ['./src/api/v2/**/*.ts'],
};

export const swaggerSpecV2 = swaggerJSDoc(options);

5. 在 Express 中掛載多版本路由與 Swagger UI

// src/app.ts
import express from 'express';
import { router as v1Router } from './api/v1/user.routes';
import { router as v2Router } from './api/v2/user.routes';
import swaggerUi from 'swagger-ui-express';
import { swaggerSpecV1 } from './swagger/v1.swagger';
import { swaggerSpecV2 } from './swagger/v2.swagger';

const app = express();

// 版本化路由
app.use('/api/v1', v1Router);
app.use('/api/v2', v2Router);

// 版本化文件
app.use('/docs/v1', swaggerUi.serve, swaggerUi.setup(swaggerSpecV1));
app.use('/docs/v2', swaggerUi.serve, swaggerUi.setup(swaggerSpecV2));

export default app;

6. JSDoc 註解範例(v1)

// src/api/v1/user.controller.ts
import { Request, Response } from 'express';

/**
 * @openapi
 * /users:
 *   get:
 *     tags:
 *       - User
 *     summary: 取得所有使用者(v1)
 *     responses:
 *       200:
 *         description: 成功回傳使用者陣列
 *         content:
 *           application/json:
 *             schema:
 *               type: array
 *               items:
 *                 $ref: '#/components/schemas/UserV1'
 */
export const getAllUsers = async (req: Request, res: Response) => {
  // 假資料
  const users = [{ id: 1, name: 'Alice' }];
  res.json(users);
};

7. JSDoc 註解範例(v2)— 新增欄位 email

// src/api/v2/user.controller.ts
import { Request, Response } from 'express';

/**
 * @openapi
 * /users:
 *   get:
 *     tags:
 *       - User
 *     summary: 取得所有使用者(v2),包含 email 欄位
 *     responses:
 *       200:
 *         description: 成功回傳使用者陣列
 *         content:
 *           application/json:
 *             schema:
 *               type: array
 *               items:
 *                 $ref: '#/components/schemas/UserV2'
 */
export const getAllUsersV2 = async (req: Request, res: Response) => {
  const users = [{ id: 1, name: 'Alice', email: 'alice@example.com' }];
  res.json(users);
};

8. 共用的 Swagger Components(可放在 components.ts

// src/swagger/components.ts
export const components = {
  schemas: {
    UserV1: {
      type: 'object',
      properties: {
        id: { type: 'integer', example: 1 },
        name: { type: 'string', example: 'Alice' },
      },
      required: ['id', 'name'],
    },
    UserV2: {
      allOf: [{ $ref: '#/components/schemas/UserV1' }],
      type: 'object',
      properties: {
        email: { type: 'string', format: 'email', example: 'alice@example.com' },
      },
      required: ['email'],
    },
  },
};

在每個 swagger-*.ts 中透過 ...components 合併即可,避免重複定義。


常見陷阱與最佳實踐

陷阱 說明 解決方案
路由版本寫死在程式碼 直接在 app.use('/api/v1', ...),若未加統一前綴,容易遺漏 使用 環境變數設定檔 (config.apiVersion) 統一管理
Swagger 文檔與實際路由不一致 手動維護 serverspaths 時常出錯 swagger-jsdoc 直接掃描路由檔案的 JSDoc,保持 一源真相
版本升級時遺忘刪除舊檔 舊版檔案堆積,增加維護成本 在 CI/CD 中加入 檢查腳本,提醒開發者是否仍需保留舊版
跨版本共用模型時衝突 兩個版本的 User 結構不同,導致 UI 顯示錯誤 使用 allOfoneOf 方式在 components 中建立繼承關係
文件過於龐大 多版本同時載入會使 Swagger UI 變慢 分別/docs/v1/docs/v2 提供不同文件,僅載入需要的版本

最佳實踐

  1. 版本前綴統一:所有路由、測試、文件均以 v{n} 開頭。
  2. 自動化測試:在單元/整合測試中驗證每個版本的回傳結構。
  3. 語意化版本號:遵循 SemVer(MAJOR.MINOR.PATCH),僅在 MAJOR 變更時才升級路由前綴。
  4. 文件分層:核心模型放在共用 components,每個版本只寫差異部份。
  5. CI/CD 部署策略:新版上線前,先在測試環境佈署 v2,確保舊版 (v1) 不受影響,再逐步淘汰。

實際應用場景

1. SaaS 平台的功能迭代

一家提供訂閱制服務的 SaaS,原本的帳戶 API 為 v1,僅支援 GET /accounts。當加入「帳戶類型」與「帳單資訊」時,開發團隊在 v2 中新增 typebillingInfo 欄位,並同時保留 v1 讓尚未升級的客戶端繼續運作。透過上述目錄與 Swagger 設定,前端工程師只需要切換 /docs/v2 即可看到新欄位與說明。

2. 行動 App 與 Web 前端共用後端

行動端因為升級頻率較慢,需要維持舊版 API;而 Web 前端則可以立即使用最新功能。將路由分為 /api/v1(行動端)與 /api/v2(Web 前端),並在 CI 中針對兩個版本跑不同的端到端測試,確保 不會因為其中一個版本的變更而破壞另一端

3. 第三方合作夥伴的 API 授權

當公司向合作夥伴提供 API 時,往往需要 契約(contract)穩定。使用版本化的 Swagger 文件,合作夥伴可以在合約上明確指定「本系統僅支援 v1」,而未來若要升級到 v2,雙方只需要簽署新版合約,避免突發的相容性問題。


總結

  • API 版本管理 是保障服務穩定與持續演進的關鍵。
  • 透過 Express + TypeScript 的目錄結構與 swagger-jsdocswagger-ui-express,我們可以快速產出 多版本的 OpenAPI 規格,讓文件與實作保持同步。
  • 共用 Components語意化版本號、以及 自動化測試,是避免版本衝突與維護負擔的最佳實踐。
  • 在實務上,無論是 SaaS、行動 App,或是第三方夥伴授權,清晰的版本化文件 都能加速開發、減少溝通成本,並提升客戶對 API 的信任度。

從今天開始,把版本號寫進路由、把 Swagger 文件拆成多套,讓你的 Express API 既安全又易於演進!祝開發順利 🚀