本文 AI 產出,尚未審核
ExpressJS (TypeScript) – Routing 路由管理
主題:Router 分模組與巢狀路由(Express.Router)
簡介
在大型 Node.js 專案中,路由的組織方式直接影響程式的可維護性與可擴充性。
單一的 app.ts 內塞滿所有路由會讓程式變得雜亂,難以定位問題,也不利於多人協作。
利用 Express.Router 進行 模組化 與 巢狀路由,不僅可以把功能區塊切割成獨立檔案,還能讓子路由自動繼承父層的 URL 前綴與中間件,讓 API 結構更直觀、測試更容易。
本篇文章將以 TypeScript 為語言,說明如何使用 Express.Router 進行路由分模組、建立巢狀路由,並提供實務範例、常見陷阱與最佳實踐,協助你從初學者快速成長為中級開發者。
核心概念
1. 為什麼要使用 Express.Router?
- 模組化:每個功能(如使用者、商品、訂單)可以各自擁有自己的 router 檔案,降低耦合度。
- 巢狀路由:子路由自動掛在父路由的路徑上,讓路徑層級清晰。
- 中間件繼承:父路由掛上的中間件會自動套用到子路由,減少重複程式碼。
2. 建立基本的 Router
// src/routes/userRouter.ts
import { Router, Request, Response, NextFunction } from 'express';
const userRouter = Router();
// 取得所有使用者
userRouter.get('/', async (req: Request, res: Response, next: NextFunction) => {
// 假設有 UserService 處理資料存取
const users = await UserService.findAll();
res.json(users);
});
// 取得單一使用者
userRouter.get('/:id', async (req: Request, res: Response, next: NextFunction) => {
const user = await UserService.findById(req.params.id);
if (!user) return res.status(404).json({ message: 'User not found' });
res.json(user);
});
export default userRouter;
重點:在 TypeScript 中使用
Router時,建議明確指定Request、Response、NextFunction的型別,能在編譯階段捕捉錯誤。
3. 在主程式掛載 Router
// src/app.ts
import express from 'express';
import userRouter from './routes/userRouter';
import productRouter from './routes/productRouter';
const app = express();
app.use(express.json());
// 以 /api/users 為前綴掛載 userRouter
app.use('/api/users', userRouter);
// 以 /api/products 為前綴掛載 productRouter
app.use('/api/products', productRouter);
export default app;
技巧:
app.use('/api/users', userRouter)會自動把userRouter裡的路徑(如/、/:id)映射為/api/users/、/api/users/:id。
4. 巢狀路由(Nested Router)
有時候功能需要更細的層級,例如 商品評論:
// src/routes/reviewRouter.ts
import { Router, Request, Response } from 'express';
const reviewRouter = Router({ mergeParams: true }); // 讓子路由可以取得父層參數
// 取得某商品的所有評論
reviewRouter.get('/', async (req: Request, res: Response) => {
const { productId } = req.params; // 來自父路由
const reviews = await ReviewService.findByProduct(productId);
res.json(reviews);
});
// 新增評論
reviewRouter.post('/', async (req: Request, res: Response) => {
const { productId } = req.params;
const newReview = await ReviewService.create(productId, req.body);
res.status(201).json(newReview);
});
export default reviewRouter;
// src/routes/productRouter.ts
import { Router } from 'express';
import reviewRouter from './reviewRouter';
const productRouter = Router();
// 取得所有商品
productRouter.get('/', async (req, res) => {
const products = await ProductService.findAll();
res.json(products);
});
// 單一商品
productRouter.get('/:productId', async (req, res) => {
const product = await ProductService.findById(req.params.productId);
if (!product) return res.status(404).json({ message: 'Product not found' });
res.json(product);
});
/* ★ 巢狀路由掛載 ★ */
productRouter.use('/:productId/reviews', reviewRouter);
export default productRouter;
說明:
reviewRouter在建立時傳入{ mergeParams: true },讓它可以存取父路由productRouter中的:productId參數。productRouter.use('/:productId/reviews', reviewRouter)把所有評論相關的路由掛在/api/products/:productId/reviews下,形成清晰的階層結構。
5. 中間件的繼承與局部使用
// src/middleware/auth.ts
import { Request, Response, NextFunction } from 'express';
export const verifyToken = (req: Request, res: Response, next: NextFunction) => {
const token = req.headers.authorization?.split(' ')[1];
if (!token) return res.status(401).json({ message: 'Missing token' });
// 假設有 JWT 驗證函式
try {
const payload = jwt.verify(token, process.env.JWT_SECRET!);
(req as any).user = payload;
next();
} catch (err) {
res.status(403).json({ message: 'Invalid token' });
}
};
// src/routes/adminRouter.ts
import { Router } from 'express';
import { verifyToken } from '../middleware/auth';
const adminRouter = Router();
// 只要在 adminRouter 上掛載 verifyToken,所有子路由都會自動套用
adminRouter.use(verifyToken);
// 取得所有使用者(管理員專屬)
adminRouter.get('/users', async (req, res) => {
const users = await UserService.findAll();
res.json(users);
});
export default adminRouter;
要點:將驗證或授權中間件掛在父路由上,可避免在每個子路由重複寫
router.use(verifyToken),保持程式碼乾淨。
常見陷阱與最佳實踐
| 陷阱 | 可能的問題 | 解決方案 / 最佳實踐 |
|---|---|---|
忘記 mergeParams: true |
子路由無法取得父層參數(req.params 為空) |
在建立子 Router 時傳入 { mergeParams: true } |
| 路徑重複掛載 | 同一路徑被多次 app.use,導致中間件執行多次 |
統一管理路由掛載位置,使用模組化的 index.ts 匯出所有 router |
| 中間件順序錯誤 | 認證中間件在路由之前未掛載,導致未授權請求直接通過 | 確保 app.use 的順序:先掛載全域中間件(如 express.json()),再掛載驗證、最後掛載路由 |
| Route Parameter 名稱衝突 | 父子路由同時使用 :id,導致混淆 |
建議使用具體語意的參數名稱(如 :userId、:productId),或在子路由使用 mergeParams 並自行取別名 |
| 未捕獲非同步錯誤 | async 路由拋出錯誤卻未傳遞至 error‑handler,導致程式崩潰 |
使用 express-async-errors 套件或自行包裝 asyncHandler 讓錯誤自動傳遞 |
最佳實踐:
- 統一入口:在
src/routes/index.ts中匯出所有 router,主程式只需要import * as routes from './routes',保持app.ts簡潔。 - 使用 TypeScript 型別:為每個路由的
req.body、req.params建立介面(interface),提升 IDE 補全與編譯安全。 - 分層目錄:
routes/、controllers/、services/分開管理,router 只負責路徑與中間件,控制器負責請求處理,service 負責業務邏輯。 - 單元測試:使用
supertest搭配 Jest 測試 router,確保每條路由的行為符合預期。
實際應用場景
1. 電商平台的 API
- 主路由:
/api/products、/api/orders、/api/users - 巢狀路由:
/api/products/:productId/reviews、/api/orders/:orderId/items - 中間件:購物車相關路由掛載
verifyCartToken,管理員路由掛載verifyAdmin。
2. 多租戶 SaaS 系統
- 租戶前綴:
/api/:tenantId/(租戶 ID 為第一層參數) - 子路由:
/api/:tenantId/projects、/api/:tenantId/projects/:projectId/tasks - 實作:在根 router 加入
tenantResolver中間件,根據tenantId取得對應資料庫連線,再使用mergeParams讓子路由可取得tenantId。
3. 微服務 Gateway
- 單一入口:
gatewayRouter作為 API Gateway,根據不同路徑轉發至內部微服務。 - 範例:
gatewayRouter.use('/auth', authServiceRouter); gatewayRouter.use('/payment', paymentServiceRouter); - 好處:每個微服務只需要管理自己的 router,Gateway 負責統一的路由掛載與跨服務的共用中間件(如日誌、速率限制)。
總結
- Express.Router 是建構可維護、可擴充 API 的核心工具,透過 模組化 與 巢狀路由 能讓程式碼結構清晰、職責分離。
- 設定
{ mergeParams: true }、合理命名參數、正確掛載中間件是避免常見問題的關鍵。 - 在 TypeScript 環境下,務必為
Request、Response、req.body、req.params等加上型別,提升開發效率與程式安全性。 - 實務上,無論是電商平台、多租戶 SaaS 或微服務 Gateway,都能從這套路由分模組的技巧中受益,讓團隊更容易協作、測試與部署。
掌握 Router 的模組化與巢狀設計,就等於掌握了 Express 應用的骨架結構。 只要遵循本文的最佳實踐,未來在面對更複雜的需求時,你也能輕鬆擴充、維護,寫出乾淨且具備彈性的後端程式碼。祝開發順利!