本文 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 時,建議明確指定 RequestResponseNextFunction 的型別,能在編譯階段捕捉錯誤。

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 讓錯誤自動傳遞

最佳實踐

  1. 統一入口:在 src/routes/index.ts 中匯出所有 router,主程式只需要 import * as routes from './routes',保持 app.ts 簡潔。
  2. 使用 TypeScript 型別:為每個路由的 req.bodyreq.params 建立介面(interface),提升 IDE 補全與編譯安全。
  3. 分層目錄routes/controllers/services/ 分開管理,router 只負責路徑與中間件,控制器負責請求處理,service 負責業務邏輯。
  4. 單元測試:使用 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 環境下,務必為 RequestResponsereq.bodyreq.params 等加上型別,提升開發效率與程式安全性。
  • 實務上,無論是電商平台、多租戶 SaaS 或微服務 Gateway,都能從這套路由分模組的技巧中受益,讓團隊更容易協作、測試與部署。

掌握 Router 的模組化與巢狀設計,就等於掌握了 Express 應用的骨架結構。 只要遵循本文的最佳實踐,未來在面對更複雜的需求時,你也能輕鬆擴充、維護,寫出乾淨且具備彈性的後端程式碼。祝開發順利!