ExpressJS (TypeScript) – Routing 路由管理
主題:基本路由建立
簡介
在 Web 應用程式的開發過程中,路由 (Routing) 是負責把「使用者的請求」對應到「程式碼的處理函式」的關鍵機制。沒有良好的路由設計,API 會變得難以維護、測試與擴充。
Express 是 Node.js 生態系中最受歡迎的 Web 框架,而 TypeScript 為 JavaScript 加上了靜態類型,讓開發者在編寫路由時能即時捕捉錯誤、提升 IDE 補全與程式碼可讀性。因此,掌握 「Express + TypeScript」的基本路由建立,是進一步開發 RESTful API、SPA 後端或微服務的第一步。
本篇文章將以 清晰、實用 的方式說明如何在 TypeScript 專案中建立與管理路由,並提供多個範例、常見陷阱與最佳實踐,讓你能快速上手並在實務專案中自信運用。
核心概念
1. Express 應用與 Router 的差別
| 概念 | app |
Router |
|---|---|---|
| 目的 | 整個 Express 應用的入口點,負責全局中介軟體與錯誤處理 | 可視為「子應用」或「模組化的路由集合」 |
| 使用時機 | 只建立一次,通常放在 src/index.ts |
需要將相關路由分組時使用,例如 users, products 等 |
| 範例 | app.get('/status', ...) |
router.get('/profile', ...),再掛到 app.use('/users', router) |
小技巧:在大型專案中,盡量把每個功能模組的路由寫在獨立的
router檔案,保持app.ts只負責設定全局中介軟體與錯誤處理。
2. TypeScript 中的型別宣告
Request、Response、NextFunction:從express套件匯入的型別,提供完整的屬性與方法提示。- 自訂 Request 參數:若要在
req.params、req.body等加入自訂欄位,需要使用 介面 (interface) 擴充。
import { Request } from 'express';
export interface CreateUserBody {
name: string;
email: string;
age?: number;
}
/* 將 body 型別套用到 Request */
export type CreateUserRequest = Request<{}, {}, CreateUserBody>;
3. 基本路由語法
router.get(path, handler);
router.post(path, handler);
router.put(path, handler);
router.delete(path, handler);
path:支援字串、正則表達式、或路由參數 (/users/:id)。handler:(req, res, next) => {},在 TypeScript 中可以給予完整型別。
程式碼範例
以下示範 5 個常見且實用的路由建立方式,每段程式碼皆附上說明與型別註解。
範例 1:最簡單的 GET 路由
// src/routes/status.ts
import { Router, Request, Response } from 'express';
const router = Router();
/**
* 取得伺服器狀態
* GET /status
*/
router.get('/status', (req: Request, res: Response) => {
res.json({ ok: true, timestamp: Date.now() });
});
export default router;
說明:此路由不需要任何參數,直接回傳 JSON。使用
Router可以在index.ts中掛載:app.use('/', statusRouter);
範例 2:帶參數的路由與型別安全
// src/routes/users.ts
import { Router, Request, Response, NextFunction } from 'express';
// 1. 定義參數型別
interface UserParams {
id: string; // 由 Express 解析為字串
}
// 2. 建立 Router
const router = Router();
/**
* 取得單一使用者資訊
* GET /users/:id
*/
router.get('/:id', (req: Request<UserParams>, res: Response, next: NextFunction) => {
const userId = req.params.id;
// 假設從資料庫取得資料
const user = { id: userId, name: 'Alice' };
if (!user) {
return res.status(404).json({ error: 'User not found' });
}
res.json(user);
});
export default router;
重點:使用
Request<UserParams>讓req.params.id在編譯時即被識別為string,避免因型別錯誤導致的執行時例外。
範例 3:POST 路由與 Body 驗證(使用 class-validator)
// src/routes/products.ts
import { Router, Request, Response } from 'express';
import { validate, IsString, IsNumber, IsOptional } from 'class-validator';
import { plainToInstance } from 'class-transformer';
class CreateProductDto {
@IsString()
name!: string;
@IsNumber()
price!: number;
@IsOptional()
@IsString()
description?: string;
}
const router = Router();
/**
* 新增商品
* POST /products
*/
router.post('/', async (req: Request, res: Response) => {
const dto = plainToInstance(CreateProductDto, req.body);
const errors = await validate(dto);
if (errors.length > 0) {
return res.status(400).json({ errors });
}
// 這裡通常會寫入資料庫
const created = { id: Date.now(), ...dto };
res.status(201).json(created);
});
export default router;
說明:透過
class-validator+class-transformer,在 TypeScript 中實現 DTO(資料傳輸物件) 的驗證,讓 API 更安全、錯誤回報更友善。
範例 4:使用 Middleware 處理驗證與授權
// src/middleware/auth.ts
import { Request, Response, NextFunction } from 'express';
/**
* 假設的驗證中介軟體
* 若 token 無效則回傳 401
*/
export function authGuard(req: Request, res: Response, next: NextFunction) {
const authHeader = req.headers.authorization;
if (!authHeader?.startsWith('Bearer ')) {
return res.status(401).json({ error: 'Missing token' });
}
const token = authHeader.split(' ')[1];
// 這裡可放 JWT 驗證邏輯
if (token !== 'valid-token') {
return res.status(403).json({ error: 'Invalid token' });
}
// token 合法,繼續往下走
next();
}
// src/routes/profile.ts
import { Router, Request, Response } from 'express';
import { authGuard } from '../middleware/auth';
const router = Router();
/**
* 取得使用者個人資料(需要授權)
* GET /profile
*/
router.get('/', authGuard, (req: Request, res: Response) => {
// 假設從 token 解碼出使用者資訊
const user = { id: '123', name: 'Bob' };
res.json(user);
});
export default router;
關鍵:將授權邏輯抽離成 middleware,讓路由保持 單一責任,且可在多個路由間重複使用。
範例 5:分層路由(Nested Router)
// src/routes/admin/users.ts
import { Router, Request, Response } from 'express';
const router = Router();
/**
* 管理員 - 取得所有使用者
* GET /admin/users
*/
router.get('/', (req: Request, res: Response) => {
// 假設回傳使用者清單
res.json([{ id: '1', name: 'Alice' }, { id: '2', name: 'Bob' }]);
});
export default router;
// src/routes/admin/index.ts
import { Router } from 'express';
import usersRouter from './users';
const router = Router();
// 以 /admin 為基礎路徑,掛載子路由
router.use('/users', usersRouter);
export default router;
// src/app.ts
import express from 'express';
import statusRouter from './routes/status';
import usersRouter from './routes/users';
import adminRouter from './routes/admin';
const app = express();
app.use(express.json());
// 全局路由掛載
app.use('/', statusRouter);
app.use('/users', usersRouter);
app.use('/admin', adminRouter);
export default app;
實務意義:當應用程式規模增長,Nested Router 能讓路徑結構清晰、檔案分層易於維護。
常見陷阱與最佳實踐
| 陷阱 | 說明 | 最佳實踐 |
|---|---|---|
忘記 express.json() |
POST/PUT 時 req.body 為 undefined |
在 app.ts 最上方加入 app.use(express.json()); |
| 路由參數未定義型別 | req.params.id 會是 any,失去 TypeScript 好處 |
使用 Request<Params> 介面或 as const 方式明確型別 |
| 中介軟體順序錯誤 | 授權中介軟體放在路由之後,導致未被觸發 | 先掛載 中介軟體,再掛載實際路由 |
| 過度寫在同一檔 | 所有路由集中於 app.ts,難以維護 |
模組化:每個功能領域獨立 router 檔案 |
| 未處理未捕獲的錯誤 | 異步函式拋出例外會讓程式崩潰 | 使用 express-async-errors 或自行捕獲 try/catch,並傳遞給錯誤處理 middleware |
進階最佳實踐
- 使用
class-validator+ DTO:讓 API 輸入驗證與型別同步。 - 自訂錯誤類別:例如
NotFoundError,ValidationError,在全局錯誤中介軟體中統一回傳。 - 加入 Swagger (OpenAPI) 文件:使用
swagger-jsdoc+swagger-ui-express,自動產生 API 文件,提升團隊協作。 - 測試驅動開發 (TDD):利用
supertest搭配jest,為每條路由寫單元測試,確保行為不被回歸破壞。 - 環境變數管理:將路由前綴(如
/api/v1)放入.env,讓不同環境的路徑保持一致。
實際應用場景
| 場景 | 需求 | 路由設計示例 |
|---|---|---|
| 電商平台 | 商品列表、下單、使用者購物車 | /api/v1/products, /api/v1/orders, /api/v1/cart(分別使用 Router) |
| 部落格系統 | 文章 CRUD、分類、標籤、評論 | /blog/posts, /blog/categories, /blog/tags, /blog/comments |
| 行動 App 後端 | 使用者登入、JWT 驗證、推播設定 | /auth/login, /auth/refresh, /users/me(使用 authGuard) |
| 微服務架構 | 每個服務只負責單一領域 | 服務 A:/service-a/users, 服務 B:/service-b/payments,透過 API Gateway 轉發 |
| 內部管理系統 | 多層級權限、資料匯出 | /admin/users, /admin/reports, 使用 roleGuard 中介軟體檢查權限 |
案例說明:在電商平台中,商品路由會使用 分層路由(
/api/v1/products/:id/reviews),同時在POST /orders前加入 授權與驗證 中介軟體,確保只有已登入且購物車內有商品的使用者才能下單。
總結
- 路由是 Express 應用的神經中樞,透過
Router可以將功能模組化、維護性提升。 - 在 TypeScript 中,利用介面與型別參數 (
Request<Params, ResBody, ReqBody>) 讓路由參數、請求體、回應體皆具備編譯時檢查,減少執行時錯誤。 - DTO + class-validator 為輸入驗證提供一致且可重用的解決方案;middleware 則負責授權、錯誤處理與共用邏輯,使路由保持單一職責。
- 面對大型專案,分層路由、環境變數管理、Swagger 文件 與 測試 是不可或缺的配套工具。
掌握了本文的 基本路由建立 後,你就能在任何 Node.js + TypeScript 專案中,快速構建乾淨、可擴充且安全的 API。接下來可以進一步探索 路由參數驗證、動態路由、以及與資料庫的整合,讓你的後端開發之路更加順暢。祝開發愉快!