本文 AI 產出,尚未審核
ExpressJS (TypeScript) – 實戰專案:完整 API 服務
主題:加入測試與 Swagger
簡介
在完成一個 RESTful API 後,最常被忽略的兩件事是 自動化測試 與 文件化。
測試能確保程式在未來的修改中不會意外破壞既有功能,而 Swagger(OpenAPI)則讓前端、測試人員,甚至是第三方開發者,能即時看到 API 的介面規格,減少溝通成本。
本篇文章將以 ExpressJS + TypeScript 為基礎,示範如何在已完成的專案中快速導入 Jest + SuperTest 進行單元/整合測試,並使用 swagger-jsdoc 與 swagger-ui-express 產生可互動的 API 文件。文章內容適合剛接觸測試的初學者,也能讓已有經驗的中階開發者快速落地實務需求。
核心概念
1️⃣ 為什麼要在 TypeScript 專案中加入測試?
- 型別安全 + 行為驗證:TypeScript 能防止型別錯誤,但不保證程式邏輯正確;測試補足這一層。
- CI/CD 整合:在 GitHub Actions、GitLab CI 等平台上跑測試,可在合併前即發現回歸。
- 文件即測試:測試程式碼本身也能成為 API 使用方式的範例,提升可讀性。
2️⃣ Jest 與 SuperTest 的角色
| 套件 | 功能 | 為何選擇 |
|---|---|---|
| Jest | 測試執行器、斷言庫、Mock 支援 | 官方支援 TypeScript、設定簡單、執行速度快 |
| SuperTest | 對 Express 伺服器發送 HTTP 請求 | 直接對 app 實例測試,不需要實際啟動 server |
3️⃣ Swagger(OpenAPI)概念
- swagger-jsdoc:從程式碼中的 JSDoc 註解產生 OpenAPI JSON。
- swagger-ui-express:將產生的 JSON 以互動 UI 呈現在
/api-docs路徑。 - 好處:自動同步文件與程式碼、支援 API 測試、可直接產生 client SDK。
程式碼範例
以下範例均以 TypeScript 撰寫,檔案結構簡化如下:
src/
├─ app.ts
├─ routes/
│ └─ user.route.ts
├─ controllers/
│ └─ user.controller.ts
├─ docs/
│ └─ swagger.ts
└─ tests/
└─ user.route.test.ts
1️⃣ 基本 Express 設定(app.ts)
// src/app.ts
import express, { Application } from 'express';
import bodyParser from 'body-parser';
import userRouter from './routes/user.route';
import swaggerSetup from './docs/swagger';
const app: Application = express();
app.use(bodyParser.json());
// ★ 1️⃣ 先掛載路由
app.use('/api/users', userRouter);
// ★ 2️⃣ 再掛載 Swagger UI
swaggerSetup(app);
export default app;
說明:先把 API 路由掛載,最後再呼叫
swaggerSetup,確保所有路由的 JSDoc 註解都已被讀取。
2️⃣ 使用 JSDoc 撰寫 Swagger 註解(user.controller.ts)
// src/controllers/user.controller.ts
import { Request, Response } from 'express';
/**
* @swagger
* /api/users:
* get:
* summary: 取得使用者列表
* tags:
* - Users
* responses:
* 200:
* description: 成功回傳使用者陣列
* content:
* application/json:
* schema:
* type: array
* items:
* $ref: '#/components/schemas/User'
*/
export const getAllUsers = async (_req: Request, res: Response) => {
// 假資料,實務上會從 DB 讀取
const users = [
{ id: 1, name: 'Alice', email: 'alice@example.com' },
{ id: 2, name: 'Bob', email: 'bob@example.com' },
];
res.json(users);
};
/**
* @swagger
* /api/users/{id}:
* get:
* summary: 取得單一使用者
* tags:
* - Users
* parameters:
* - in: path
* name: id
* required: true
* schema:
* type: integer
* description: 使用者 ID
* responses:
* 200:
* description: 單一使用者資料
* 404:
* description: 找不到使用者
*/
export const getUserById = async (req: Request, res: Response) => {
const id = Number(req.params.id);
if (id === 1) {
res.json({ id: 1, name: 'Alice', email: 'alice@example.com' });
} else {
res.status(404).json({ message: 'User not found' });
}
};
重點:
@swagger註解直接寫在 controller 上,swagger-jsdoc 會在執行swaggerSetup時自動抽取。
3️⃣ 建立路由(user.route.ts)
// src/routes/user.route.ts
import { Router } from 'express';
import { getAllUsers, getUserById } from '../controllers/user.controller';
const router = Router();
router.get('/', getAllUsers);
router.get('/:id', getUserById);
export default router;
4️⃣ Swagger 設定(swagger.ts)
// src/docs/swagger.ts
import { Application } from 'express';
import swaggerJsdoc from 'swagger-jsdoc';
import swaggerUi from 'swagger-ui-express';
/**
* 初始化 Swagger 並掛載到 Express
*/
export default function swaggerSetup(app: Application) {
const options = {
definition: {
openapi: '3.0.0',
info: {
title: 'ExpressJS TypeScript API',
version: '1.0.0',
description: '範例 API,示範如何結合 Swagger 與 Jest 測試',
},
servers: [
{
url: 'http://localhost:3000',
},
],
components: {
schemas: {
User: {
type: 'object',
properties: {
id: { type: 'integer', example: 1 },
name: { type: 'string', example: 'Alice' },
email: { type: 'string', example: 'alice@example.com' },
},
},
},
},
},
// 讀取所有含 @swagger 註解的檔案
apis: ['./src/**/*.ts'],
};
const swaggerSpec = swaggerJsdoc(options);
// 以 /api-docs 為路徑提供 UI
app.use('/api-docs', swaggerUi.serve, swaggerUi.setup(swaggerSpec));
}
技巧:
apis欄位使用 glob pattern,一次就能抓取所有路由與 controller 中的註解,免除手動維護。
5️⃣ Jest + SuperTest 測試(user.route.test.ts)
// src/tests/user.route.test.ts
import request from 'supertest';
import app from '../app';
describe('User API 測試', () => {
/** 測試 GET /api/users */
it('GET /api/users 應回傳使用者陣列', async () => {
const res = await request(app).get('/api/users').expect(200);
expect(Array.isArray(res.body)).toBe(true);
expect(res.body).toHaveLength(2);
expect(res.body[0]).toHaveProperty('id');
});
/** 測試 GET /api/users/:id 成功情況 */
it('GET /api/users/1 應回傳單一使用者', async () => {
const res = await request(app).get('/api/users/1').expect(200);
expect(res.body).toMatchObject({
id: 1,
name: 'Alice',
});
});
/** 測試 GET /api/users/:id 錯誤情況 */
it('GET /api/users/999 應回傳 404', async () => {
const res = await request(app).get('/api/users/999').expect(404);
expect(res.body).toHaveProperty('message', 'User not found');
});
});
說明:
request(app)直接對 express 實例 發送請求,無需啟動實體伺服器。- 使用
expect斷言回傳狀態碼與回傳結構,確保 API 行為符合預期。
6️⃣ Jest 設定(jest.config.ts)
// jest.config.ts
export default {
preset: 'ts-jest',
testEnvironment: 'node',
testMatch: ['**/tests/**/*.test.ts'],
// 顯示測試覆蓋率,實務上建議至少 80%
collectCoverage: true,
coverageDirectory: 'coverage',
};
小提醒:
ts-jest允許直接以 TypeScript 撰寫測試檔,省去編譯步驟。
常見陷阱與最佳實踐
| 陷阱 | 為什麼會發生 | 解決方式 |
|---|---|---|
測試跑不出:supertest 找不到路由 |
沒有把 app 正確匯出,或在測試前未載入中間件 |
確保 app.ts export default app,且測試檔 import app |
| Swagger 文件缺少端點 | apis 路徑寫錯、或 JSDoc 註解語法不完整 |
使用相對於根目錄的 glob,並檢查 @swagger 標籤是否正確閉合 |
| 測試環境與正式環境不一致 | 測試時直接讀取 .env,但 CI 中缺少變數 |
使用 dotenv 並在 jest.setup.ts 中先載入,或在 CI 中明確設定環境變數 |
| 測試時間過長 | 每個測試都啟動完整的資料庫連線 | 針對單元測試使用 mock,整合測試才連接真實 DB;或使用 in‑memory DB(如 SQLite) |
| Swagger UI 無法載入 JSON | swagger-jsdoc 產生的 JSON 有語法錯誤 |
先在本機執行 node -e "console.log(require('./dist/docs/swagger'))" 檢查產出,或使用線上 JSON Linter |
最佳實踐
- 測試先行(Test‑First):在寫 controller 前先寫測試,能保證需求落實。
- 保持 Swagger 註解與程式碼同步:可在 PR 檢查清單加入「Swagger 註解是否更新」的步驟。
- CI 中執行測試與產生文件:在 GitHub Actions 中加入
npm run test && npm run swagger:build,確保每次合併都有最新的 API 文件。 - 使用自動化腳本檢查 coverage:設定 coverage 門檻(如 85%),失敗時阻止合併。
- 分層測試:單元測試驗證純函式邏輯、服務層;整合測試驗證路由與中間件;E2E 測試驗證完整工作流程(可使用 Cypress 或 Playwright)。
實際應用場景
| 場景 | 為什麼需要測試 + Swagger |
|---|---|
| 微服務間的 API 合約 | 服務 A 依賴服務 B 的介面;Swagger 為合約文件,測試保證合約不被破壞。 |
| 前端團隊同時開發 | 前端可直接在 Swagger UI 上測試 API,測試失敗時迅速回報,提升協作效率。 |
| 持續交付(CI/CD) | 每次部署前自動跑測試,確保不會因程式碼變更導致 500 錯誤;同時自動部署最新的 API 文件至內部文件站。 |
| 第三方合作夥伴 | 合作夥伴只需要 Swagger URL,即可生成 SDK,測試腳本亦可作為範例。 |
| 安全與合規 | 有些產業要求所有 API 必須有完整文件且通過測試,才能通過審核。 |
總結
- 測試 為 API 生命週期的防護盾,使用 Jest + SuperTest 能在 TypeScript 專案中快速寫出可靠的單元與整合測試。
- Swagger 為 API 的「說明書」與「契約」,透過 swagger-jsdoc 直接從程式碼產生,配合 swagger-ui-express 提供即時、互動的文件介面。
- 兩者結合不僅提升開發效率,也讓團隊在 CI/CD、跨部門協作、合規審核 等重要環節中更具信心。
實務建議:在專案的 第一個迭代 就導入測試與 Swagger,避免等到系統規模變大後再回頭補救。只要把「測試」與「文件」視為開發的必備步驟,就能讓 ExpressJS + TypeScript 的 API 服務保持 高可用、易維護、易擴充。祝開發順利!