本文 AI 產出,尚未審核

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 來分層管理。

最佳實踐

  1. 型別第一:所有自訂 middleware 均使用 RequestHandler / ErrorRequestHandler,並在需要時擴充 RequestResponse
  2. 保持單一職責:每個 middleware 只做一件事(例如只負責日誌、只負責驗證)。
  3. 錯誤統一化:自訂錯誤類別(class AppError extends Error { status: number }),在錯誤 middleware 中統一處理。
  4. 測試覆蓋:使用 Jest 或 Vitest 撰寫單元測試,驗證 middleware 在不同情境下的行為。
  5. 環境分離:開發環境開啟詳細日誌與 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 環境下,使用官方提供的 RequestHandlerErrorRequestHandler,並透過 全域介面擴充 讓自訂屬性(如 req.user)得到型別保護。
  • 透過 單一職責錯誤統一化測試覆蓋 等最佳實踐,可讓 middleware 更易維護、降低 Bug。
  • 實務上,日誌、驗證、速率限制、回應包裝等都是常見且必備的自訂 middleware,熟練這些技巧能讓你的 Express 專案在 可讀性、可測試性與安全性 上都有顯著提升。

從今天開始,把上述範例直接套用到你的專案,並依照實際需求自行擴充,讓 TypeScript 為你的 Express 應用提供更堅實的型別防護吧!祝開發順利 🚀