本文 AI 產出,尚未審核

ExpressJS (TypeScript) – Middleware 概念與使用

單元:Application‑level、Router‑level 中介層


簡介

在 ExpressJS 中,middleware(中介層)是所有請求與回應流程的核心。它不僅負責 攔截、處理、或改寫 請求資料,還能在請求完成前執行驗證、日誌、錯誤處理等跨功能邏輯。對於使用 TypeScript 開發的 Node.js 專案而言,善用 middleware 能讓程式碼保持 高可讀性、可重用性,同時享受到型別安全的好處。

本單元將深入探討 Application‑levelRouter‑level 兩種中介層的差異與使用時機,並提供實作範例、常見陷阱與最佳實踐,讓你能在真實專案中快速上手、正確運用。


核心概念

1. Middleware 的基本結構

在 Express 中,middleware 本質上是一個函式,簽名如下:

(req: Request, res: Response, next: NextFunction) => void | Promise<void>
  • reqres:分別代表 HTTP 請求與回應物件。
  • next:呼叫 next() 讓 Express 繼續往下尋找下一個符合條件的 middleware。若不呼叫 next(),請求將會在此「卡住」——這也是錯誤處理與回應結束的關鍵。

⚠️ 若 middleware 回傳 Promise,Express 會自動捕捉 reject 並導向錯誤處理器。


2. Application‑level Middleware

Application‑level 中介層是掛載在整個 express() 實例上,會對 所有路由(或符合特定路徑前綴)生效。常見用途包括:

  • 請求日誌(logging)
  • CORS 設定
  • 全域錯誤處理

範例 1:全域請求日誌(同步版)

import express, { Request, Response, NextFunction } from "express";

const app = express();

// 1️⃣ 全域日誌 Middleware
app.use((req: Request, res: Response, next: NextFunction) => {
  console.log(`[${new Date().toISOString()}] ${req.method} ${req.path}`);
  next(); // 必須呼叫 next(),否則請求會卡住
});

app.get("/", (req, res) => res.send("Hello World!"));
app.listen(3000);

範例 2:使用 async/await 處理驗證

app.use(async (req: Request, res: Response, next: NextFunction) => {
  try {
    const token = req.headers.authorization?.split(" ")[1];
    if (!token) throw new Error("Missing token");

    // 假設有一個 verifyToken 的 async 函式
    const payload = await verifyToken(token);
    (req as any).user = payload; // 把使用者資訊掛到 req 上
    next();
  } catch (err) {
    next(err); // 交給錯誤處理 Middleware
  }
});

範例 3:全域 CORS 設定(使用第三方套件)

import cors from "cors";

app.use(
  cors({
    origin: ["https://example.com", "http://localhost:3000"],
    methods: ["GET", "POST", "PUT", "DELETE"],
    credentials: true,
  })
);

3. Router‑level Middleware

Router‑level 中介層是掛載在 express.Router() 實例上,只對 該 Router 內的路由生效。這讓我們可以把功能模組化、依功能區分 中介層,避免全域污染。

範例 4:建立一個「使用者」Router,內部使用驗證 Middleware

import { Router, Request, Response, NextFunction } from "express";

const userRouter = Router();

// 只在 userRouter 內部使用的驗證 Middleware
function authGuard(req: Request, res: Response, next: NextFunction) {
  if ((req as any).user) {
    next();
  } else {
    res.status(401).json({ message: "Unauthorized" });
  }
}

// 公開路由:註冊
userRouter.post("/register", (req, res) => {
  // 註冊邏輯...
  res.json({ message: "User registered" });
});

// 受保護路由:取得個人資訊
userRouter.get("/me", authGuard, (req, res) => {
  res.json({ user: (req as any).user });
});

export default userRouter;

在主程式中掛載:

import userRouter from "./routes/user";

app.use("/api/users", userRouter); // 前綴為 /api/users

範例 5:使用 Router‑level 的錯誤處理

// 放在 router 最後的錯誤 Middleware
userRouter.use((err: any, req: Request, res: Response, next: NextFunction) => {
  console.error("UserRouter error:", err);
  res.status(500).json({ message: "Server error in UserRouter" });
});

範例 6:結合多層 Router(子路由)

const adminRouter = Router();

// 只給 admin 使用的 Middleware
adminRouter.use((req, res, next) => {
  if ((req as any).user?.role === "admin") next();
  else res.status(403).json({ message: "Forbidden" });
});

adminRouter.get("/dashboard", (req, res) => {
  res.json({ stats: "Some admin data" });
});

// 把 adminRouter 掛在 userRouter 之下
userRouter.use("/admin", adminRouter);

常見陷阱與最佳實踐

陷阱 說明 解決方式
忘記呼叫 next() 中介層不呼叫 next(),請求永遠停在此處,導致客戶端超時。 一定在非回應結束的情況下呼叫 next();若是回傳 res.send(),則不必。
同步錯誤不會自動傳遞 在同步程式碼中拋出錯誤不會被 Express 捕捉。 使用 try/catch,或把同步函式包成 async
型別不匹配 TypeScript 預設的 Request 介面沒有自訂屬性(如 req.user)。 使用 模組擴充 (declare module "express-serve-static-core") 或 as any 暫時繞過。
全域 Middleware 太重 把所有驗證、日誌都寫在全域,會降低效能且難以維護。 把與特定領域相關的邏輯搬到 Router‑level,僅保留真正全域需求。
錯誤處理 Middleware 順序錯誤 錯誤處理函式必須放在所有路由與普通 Middleware 後面。 確保 app.use(errorHandler) 位於檔案最底部。

最佳實踐

  1. 模組化:每個功能領域(User、Product、Admin…)建立獨立的 Router,並在內部掛載專屬 Middleware。
  2. 型別安全:使用 declare globaldeclare modulereqres 加上自訂屬性,讓 TypeScript 能正確檢查。
  3. 錯誤傳遞:只要有錯誤發生,務必 next(err),讓最後的錯誤處理器統一回應。
  4. 非阻塞:盡量使用 非同步async/await)寫法,避免阻塞 Event Loop。
  5. 測試:使用 supertest 搭配 Jest 撰寫 middleware 單元測試,確保路由與錯誤流程如預期。

實際應用場景

  1. API 金鑰驗證
    • Application‑level 設定一次性檢查 API 金鑰,所有入口都必須通過。
  2. 多租戶(Tenant)系統
    • 使用 Router‑level 中介層根據 URL 前綴 (/tenant/:id/...) 讀取租戶設定,僅對該租戶的路由套用。
  3. 速率限制(Rate Limiting)
    • 針對特定路由(如登入)加入 express-rate-limit,只在該 Router 上使用,避免全站過度限制。
  4. 檔案上傳與驗證
    • Router‑level 使用 multer 處理 /upload 子路由,其他路由則不受影響。
  5. 動態權限檢查
    • 把權限檢查寫成 Router‑level Middleware,讓每個子路由只需要宣告需要的權限代號即可。

總結

  • Application‑level Middleware 為全域性需求服務,適合日誌、CORS、全站驗證等。
  • Router‑level Middleware 則提供了 功能模組化 的能力,讓每個子系統可以自行決定要套用哪些前置或後置處理。
  • 在 TypeScript 專案中,利用型別擴充與 async/await,不僅能提升開發效率,還能降低執行時錯誤。
  • 正確的 next() 呼叫、錯誤傳遞順序與適當的模組化設計,是建構可維護、可擴充 Express 應用的關鍵。

掌握了上述概念與實作技巧之後,你就能在 Express+TypeScript 專案中,靈活運用 Application‑levelRouter‑level 中介層,寫出乾淨、可測試且易於維護的後端服務。祝開發順利!