ExpressJS (TypeScript) – Middleware 概念與使用
主題:建立自訂 Middleware(含 TypeScript 型別)
簡介
在 Node.js 生態系統中,Express 是最常被採用的 Web 框架,而 Middleware 則是 Express 能力的核心。它不只負責請求與回應的前置/後置處理,還提供了 模組化、可重用 的程式結構,讓大型專案的維護變得更簡單。
隨著 TypeScript 在後端開發的普及,將 型別安全 引入 Middleware 可以避免許多執行時錯誤,提升開發者的開發體驗與程式碼品質。本篇文章將從概念說明開始,帶你一步步建立 自訂 Middleware,並完整示範在 TypeScript 中如何正確宣告與使用型別。
核心概念
1️⃣ Middleware 是什麼?
在 Express 中,Middleware 是一個函式,簽名通常為 (req, res, next) => void。它會在 請求(request) 進入路由之前、之後或是 錯誤發生時 被呼叫。每個 Middleware 必須在完成自己的工作後呼叫 next(),讓控制權傳遞給下一個 Middleware;若不呼叫 next(),請求就會在此中斷,這也是實作驗證、授權等功能的關鍵點。
重點:Middleware 的執行順序完全依賴於 註冊順序,因此了解「先後」對於除錯與功能設計至關重要。
2️⃣ Middleware 的類別
| 類別 | 說明 | 常見用途 |
|---|---|---|
| 一般 Middleware | (req, res, next) |
記錄、解析 body、設定 CORS、驗證等 |
| 錯誤處理 Middleware | (err, req, res, next) |
捕捉例外、回傳統一錯誤格式 |
| 路由層級 Middleware | 只掛在特定 router 上 | 針對子路由的權限檢查、參數驗證 |
| 應用層級 Middleware | 直接掛在 app 上 |
全域日誌、跨域設定、統一回應格式 |
3️⃣ 在 TypeScript 中定義 Middleware 型別
Express 已經為 TypeScript 提供了完整的型別宣告(@types/express),我們只需要 import 正確的介面即可:
import { Request, Response, NextFunction, RequestHandler, ErrorRequestHandler } from 'express';
RequestHandler:一般 Middleware 的型別ErrorRequestHandler:錯誤處理 Middleware 的型別
若要自訂額外的屬性(例如在 req 上掛載 user),需要 擴充介面:
declare global {
namespace Express {
interface Request {
/** 已驗證的使用者資訊 */
user?: { id: string; role: string };
}
}
}
技巧:使用
declare global能讓所有檔案都能感知到擴充後的Request型別,避免重複宣告。
4️⃣ 撰寫自訂 Middleware 範例
以下提供 四個 常見且實用的自訂 Middleware 範例,涵蓋 日誌、驗證、錯誤捕捉、回應統一格式。每個範例皆附上完整註解與型別說明。
4.1 日誌 Middleware(Request Logger)
import { RequestHandler } from 'express';
/**
* 在每一次請求進來時,印出 method、URL 與執行時間。
* 使用 `console.log` 僅示範,實務上建議改用 winston 或 pino。
*/
export const requestLogger: RequestHandler = (req, res, next) => {
const start = Date.now();
// 監聽 response 結束事件,計算耗時
res.on('finish', () => {
const duration = Date.now() - start;
console.log(
`[${new Date().toISOString()}] ${req.method} ${req.originalUrl} → ${res.statusCode} (${duration}ms)`
);
});
next(); // 必須呼叫 next(),否則請求會卡住
};
使用方式:
import express from 'express';
import { requestLogger } from './middleware/logger';
const app = express();
app.use(requestLogger); // 全域掛載
4.2 JWT 驗證 Middleware
import { RequestHandler } from 'express';
import jwt from 'jsonwebtoken';
const JWT_SECRET = process.env.JWT_SECRET || 'hard-to-guess-secret';
/**
* 解析 Authorization Header,驗證 JWT,並把使用者資訊掛載到 req.user。
* 若驗證失敗,直接回傳 401。
*/
export const authenticateJwt: RequestHandler = (req, res, next) => {
const authHeader = req.headers.authorization;
if (!authHeader?.startsWith('Bearer ')) {
return res.status(401).json({ message: 'Missing or malformed token' });
}
const token = authHeader.split(' ')[1];
try {
const payload = jwt.verify(token, JWT_SECRET) as { id: string; role: string };
req.user = { id: payload.id, role: payload.role }; // 透過全域介面擴充
next();
} catch (err) {
return res.status(401).json({ message: 'Invalid token' });
}
};
使用方式(只對特定路由套用):
import express from 'express';
import { authenticateJwt } from './middleware/auth';
const router = express.Router();
router.get('/profile', authenticateJwt, (req, res) => {
// 此時 req.user 已經有型別保障
res.json({ userId: req.user?.id, role: req.user?.role });
});
4.3 統一錯誤處理 Middleware
import { ErrorRequestHandler } from 'express';
/**
* 捕捉所有未被處理的錯誤,回傳統一的 JSON 結構。
* 只要把此 middleware 放在所有路由的最後面即可生效。
*/
export const errorHandler: ErrorRequestHandler = (err, _req, res, _next) => {
console.error('[Error]', err);
const status = (err as any).status || 500;
const message = (err as any).message || 'Internal Server Error';
res.status(status).json({
success: false,
error: {
message,
// 僅在開發環境顯示 stack,正式環境可省略
...(process.env.NODE_ENV === 'development' && { stack: err.stack })
}
});
};
註冊方式:
import express from 'express';
import { errorHandler } from './middleware/errorHandler';
const app = express();
// ... 其他 middleware & routes
app.use(errorHandler); // 必須放在最後
4.4 回應統一格式 Middleware(Response Wrapper)
import { RequestHandler } from 'express';
/**
* 攔截 `res.json`,將資料包裝成 { success: true, data: ... } 的格式。
* 只要在路由之前掛上此 middleware,即可省去每個 handler 手動包裝。
*/
export const responseWrapper: RequestHandler = (_req, res, next) => {
const originalJson = res.json.bind(res);
// 改寫 res.json 方法
res.json = (data: unknown) => {
return originalJson({ success: true, data });
};
next();
};
使用方式:
import express from 'express';
import { responseWrapper } from './middleware/responseWrapper';
const app = express();
app.use(responseWrapper);
app.get('/ping', (_req, res) => {
res.json('pong'); // 最終回傳 { success: true, data: 'pong' }
});
常見陷阱與最佳實踐
| 陷阱 | 說明 | 解決方案 |
|---|---|---|
忘記呼叫 next() |
中間件不呼叫 next 會導致請求卡住,客戶端永遠等待。 |
確保每條路徑都有 next(),或在回應後直接結束。 |
在錯誤處理 Middleware 前寫 next(err) |
若錯誤 middleware 放在錯誤產生之前,next(err) 會被當成普通 middleware 處理。 |
將錯誤處理 middleware 放在最末端,並使用 ErrorRequestHandler 型別。 |
在 TypeScript 中直接修改 req |
直接在程式碼裡寫 req.user = ... 會產生型別錯誤。 |
透過 declare global 擴充 Express.Request,或使用 as any(不建議)。 |
| 同步拋錯不會觸發錯誤 middleware | 在 async 函式內拋錯,若未使用 next(err),Express 不會捕捉。 |
使用 try/catch 並呼叫 next(err),或使用 express-async-errors 套件自動捕捉。 |
| 過度嵌套 Middleware | 多層嵌套會降低可讀性,且容易忘記 next() 的呼叫順序。 |
將相關功能抽成單一 middleware,或使用 router-level 來分層管理。 |
最佳實踐
- 型別第一:所有自訂 middleware 均使用
RequestHandler/ErrorRequestHandler,並在需要時擴充Request、Response。 - 保持單一職責:每個 middleware 只做一件事(例如只負責日誌、只負責驗證)。
- 錯誤統一化:自訂錯誤類別(
class AppError extends Error { status: number }),在錯誤 middleware 中統一處理。 - 測試覆蓋:使用 Jest 或 Vitest 撰寫單元測試,驗證 middleware 在不同情境下的行為。
- 環境分離:開發環境開啟詳細日誌與 stack trace,正式環境則隱藏敏感資訊。
實際應用場景
| 場景 | 可能的自訂 Middleware | 為何需要 |
|---|---|---|
| API 金鑰驗證 | apiKeyValidator(檢查 x-api-key) |
防止未授權的外部呼叫,保護資源。 |
| 多租戶系統 | tenantResolver(根據子域或 Header 決定租戶) |
在同一個服務中分離不同客戶資料。 |
| 速率限制(Rate Limiting) | rateLimiter(使用 Redis 計數) |
防止 DoS 攻擊與濫用。 |
| 檔案上傳前的檔案類型驗證 | fileTypeChecker(配合 Multer) |
確保只接受安全的檔案格式。 |
| 回傳結果的國際化 | i18nResponseWrapper(根據 Accept-Language 包裝訊息) |
讓前端可以直接取得本地化訊息。 |
範例:在多租戶系統中,我們可以先寫一個
tenantResolver,它會從req.headers['x-tenant-id']讀取租戶代號,然後把對應的資料庫連線或設定掛到req上。接著的所有路由都能安全地使用req.tenant,而不必在每個 handler 重複取得租戶資訊。
總結
- Middleware 是 Express 的靈魂,負責請求的前置、後置與錯誤處理。
- 在 TypeScript 環境下,使用官方提供的
RequestHandler、ErrorRequestHandler,並透過 全域介面擴充 讓自訂屬性(如req.user)得到型別保護。 - 透過 單一職責、錯誤統一化、測試覆蓋 等最佳實踐,可讓 middleware 更易維護、降低 Bug。
- 實務上,日誌、驗證、速率限制、回應包裝等都是常見且必備的自訂 middleware,熟練這些技巧能讓你的 Express 專案在 可讀性、可測試性與安全性 上都有顯著提升。
從今天開始,把上述範例直接套用到你的專案,並依照實際需求自行擴充,讓 TypeScript 為你的 Express 應用提供更堅實的型別防護吧!祝開發順利 🚀