本文 AI 產出,尚未審核
ExpressJS (TypeScript) – Swagger / OpenAPI 文件化
使用 swagger-ui-express
簡介
在現代的 Web API 開發流程中,文件化 已不再是事後的附加工作,而是必須與程式碼同步演進的核心需求。
透過 OpenAPI (原 Swagger) 規範,我們可以自動產生機器可讀的 API 描述,同時提供一個互動式的 UI 讓前端、測試人員或第三方開發者直接在瀏覽器測試介面。
swagger-ui-express 是官方推薦的 Express 中介軟體,結合 TypeScript 後不僅能保留型別安全,還能在編譯階段即捕捉文件描述的錯誤。
本篇教學將從安裝、基本設定、進階範例一路帶你完成一套可在開發與測試環境即時瀏覽的 API 文件。
核心概念
1. OpenAPI 與 Swagger 的關係
- OpenAPI:由 Linux 基金會維護的 API 規範標準,描述 API 的路由、參數、回傳格式等。
- Swagger:最早的實作名稱,現在通常指兩件事:
- swagger-ui – 讓 JSON/YAML 規格渲染成互動式網頁。
- swagger-jsdoc / swagger-ui-express – 在 Node/Express 中自動產生與掛載 UI 的工具。
2. 為什麼要在 TypeScript 專案使用 swagger-ui-express?
| 好處 | 說明 |
|---|---|
| 型別安全 | 使用 @types/swagger-ui-express,IDE 能即時提示設定錯誤。 |
| 自動產生 | swagger-jsdoc 會根據 JSDoc/TS 註解產出 OpenAPI JSON,減少手寫重複工作。 |
| 即時測試 | UI 內建「Try it out」功能,直接發送請求驗證路由是否正確。 |
| CI/CD 整合 | 產出的 JSON 可作為合約測試或 API 版本管理的依據。 |
3. 基本流程概覽
- 安裝相依套件
- 建立 OpenAPI 規格檔(JSON 或 YAML)或使用
swagger-jsdoc動態產生。 - 在 Express 中掛載 swagger-ui,設定路徑(如
/api-docs)。 - 在程式碼中加入註解,讓
swagger-jsdoc把路由資訊注入規格檔。 - 啟動伺服器,瀏覽器開啟
/api-docs即可看到文件。
程式碼範例
以下範例採用 TypeScript,並以 express@4、swagger-ui-express、swagger-jsdoc 為核心套件。
3.1 安裝必要套件
npm i express swagger-ui-express swagger-jsdoc
npm i -D typescript @types/express @types/swagger-ui-express ts-node-dev
Tip:
ts-node-dev方便開發時自動重啟。
3.2 建立 src/server.ts
import express, { Request, Response } from 'express';
import swaggerUi from 'swagger-ui-express';
import swaggerJSDoc from 'swagger-jsdoc';
import path from 'path';
// -------------------- 1. 建立 Express App --------------------
const app = express();
app.use(express.json());
// -------------------- 2. 設定 Swagger JSDoc --------------------
const swaggerOptions: swaggerJSDoc.Options = {
definition: {
openapi: '3.0.3',
info: {
title: 'My Express API',
version: '1.0.0',
description: '使用 swagger-ui-express 的範例文件',
},
servers: [
{ url: 'http://localhost:3000', description: '本機開發環境' },
],
},
// 指向所有使用 JSDoc 註解的檔案
apis: [path.join(__dirname, '/routes/*.ts')],
};
const swaggerSpec = swaggerJSDoc(swaggerOptions);
// -------------------- 3. 掛載 Swagger UI --------------------
app.use('/api-docs', swaggerUi.serve, swaggerUi.setup(swaggerSpec));
// -------------------- 4. 載入路由 --------------------
import userRouter from './routes/user';
app.use('/users', userRouter);
// -------------------- 5. 啟動 Server --------------------
const PORT = process.env.PORT || 3000;
app.listen(PORT, () => {
console.log(`🚀 Server listening on http://localhost:${PORT}`);
console.log(`📚 Swagger UI: http://localhost:${PORT}/api-docs`);
});
3.3 建立路由檔 src/routes/user.ts
import { Router, Request, Response } from 'express';
const router = Router();
/**
* @swagger
* /users:
* get:
* summary: 取得使用者列表
* tags: [User]
* responses:
* 200:
* description: 成功回傳使用者陣列
* content:
* application/json:
* schema:
* type: array
* items:
* $ref: '#/components/schemas/User'
*/
router.get('/', (req: Request, res: Response) => {
// 假資料
const users = [
{ id: 1, name: 'Alice', email: 'alice@example.com' },
{ id: 2, name: 'Bob', email: 'bob@example.com' },
];
res.json(users);
});
/**
* @swagger
* /users/{id}:
* get:
* summary: 依 ID 取得單一使用者
* tags: [User]
* parameters:
* - in: path
* name: id
* required: true
* schema:
* type: integer
* description: 使用者唯一識別碼
* responses:
* 200:
* description: 使用者資料
* content:
* application/json:
* schema:
* $ref: '#/components/schemas/User'
* 404:
* description: 找不到使用者
*/
router.get('/:id', (req: Request, res: Response) => {
const userId = Number(req.params.id);
const user = { id: userId, name: 'Alice', email: 'alice@example.com' };
if (userId === 1) {
res.json(user);
} else {
res.status(404).json({ message: 'User not found' });
}
});
/**
* @swagger
* components:
* schemas:
* User:
* type: object
* required:
* - id
* - name
* - email
* properties:
* id:
* type: integer
* description: 使用者編號
* name:
* type: string
* description: 使用者名稱
* email:
* type: string
* format: email
* description: 電子郵件
*/
export default router;
3.4 加入全域錯誤處理(可選)
// 放在 server.ts 最後
app.use((err: Error, _: Request, res: Response, __: any) => {
console.error(err);
res.status(500).json({ message: 'Internal Server Error' });
});
3.5 使用 npm run dev 以 ts-node-dev 啟動
// package.json scripts
{
"scripts": {
"dev": "ts-node-dev --respawn src/server.ts"
}
}
執行 npm run dev 後,瀏覽器開啟 http://localhost:3000/api-docs 即可看到 Swagger UI,且「Try it out」功能會直接呼叫上述 /users、/users/{id} 兩個端點。
常見陷阱與最佳實踐
| 陷阱 | 說明 | 解決方案 |
|---|---|---|
| 規格與程式碼不一致 | 手寫 JSON 時容易遺漏參數或回傳格式。 | 使用 swagger-jsdoc + JSDoc 註解,讓文件自動同步。 |
路由路徑忘記加 / |
app.use('/api-docs', ...) 若少了 /,會導致 UI 無法正確載入資源。 |
檢查路徑前後斜線,或使用 path.join 產生絕對路徑。 |
| Schema 重複定義 | 多個檔案都寫同樣的 components.schemas,會造成衝突。 |
把共用 schema 抽離到 src/docs/schemas.ts,在所有檔案中 $ref。 |
| 開發環境暴露機密 | 把 Swagger UI 部署到正式環境前未關閉,可能洩漏內部 API。 | 在 process.env.NODE_ENV !== 'production' 時才掛載 UI,或使用環境變數控制。 |
| 型別與 Swagger 不匹配 | TypeScript 型別是 number,但 Swagger 定義為 string。 |
兩者保持一致,或使用 ts-json-schema-generator 自動產生 schema。 |
最佳實踐清單
- 分層管理:
/docs資料夾存放全域swaggerOptions、共用 schema;路由檔內只寫與該路由相關的註解。 - 版本化:在
info.version加上語意化版本號,並在 URL 中加入v1、v2等前綴,避免舊版客戶端被破壞。 - 自動測試:使用
swagger-cli validate或openapi-validator在 CI 中驗證產出的 JSON。 - 安全性:若需要驗證,於 Swagger UI 加入
Authorization標頭(使用swagger-ui-express的swaggerOptions設定authAction)。 - 文件維護:每次新增或變更 API 時,務必同步更新 JSDoc 註解,讓 PR 檢查更具體。
實際應用場景
| 場景 | 需求 | 使用 swagger-ui-express 的好處 |
|---|---|---|
| 前後端分離開發 | 前端需要即時了解 API 參數與回傳結構。 | UI 提供即時測試,減少跨團隊溝通成本。 |
| 微服務間契約測試 | 多個服務間依賴 OpenAPI 規範。 | 產出的 JSON 可作為 contract testing(如 Pact)基礎。 |
| 第三方合作夥伴 | 對外提供 API 介面說明。 | 只要把 /api-docs 部署至公開網址,即可讓合作夥伴直接閱讀與測試。 |
| 內部工具或管理平台 | 管理員需要快速檢視與執行 CRUD 操作。 | Swagger UI 本身就提供 CRUD 測試介面,省去額外開發。 |
| 自動產生 SDK | 需要根據 OpenAPI 產生 TypeScript/JavaScript SDK。 | 有了正確的 OpenAPI JSON,可使用 openapi-generator-cli 一鍵產生 client 程式庫。 |
總結
- swagger-ui-express 結合 swagger-jsdoc 能讓 Express + TypeScript 專案在開發階段即擁有完整、互動式的 API 文件。
- 透過 JSDoc 註解,文件與程式碼同步,降低錯誤與維護成本。
- 注意 環境切換(開發 vs 正式)與 schema 共用,才能在安全與可維護性上取得最佳平衡。
- 最後,將產出的 OpenAPI 規格納入 CI、契約測試或 SDK 生成流程,讓文件成為整個開發生態的一環,而非事後補救的文件。
祝你在使用 swagger-ui-express 為 ExpressJS (TypeScript) 應用打造專業文件的路上,順利又愉快! 🚀