seansie's blog

深入理解 Express.js Middleware:從核心概念、常見套件到實戰自製

在 Node.js 與 Express.js 的架構設計中,Middleware(中介軟體) 是最核心的靈魂機制。理解 Middleware 的運作模式,是掌握後端請求生命週期、構建安全且可維護 API 的關鍵基礎。


什麼是 Middleware?

Express 本質上就是由一系列 Middleware 串接而成的路由與控制流程框架。

當客戶端發送 HTTP 請求至伺服器時,該請求並不會立即直達最終的業務邏輯處理常式(Route Handler),而是會經過一條由多個函數組成的處理管線(Pipeline)。管線中的每一個節點就是一個 Middleware。

每個標準的 Middleware 函數都可以存取三個核心物件:

  • req (Request):當前 HTTP 請求的資訊(包含 Headers、Body、Params、Query 等)。
  • res (Response):準備返回給客戶端的 HTTP 回應物件。
  • next:負責將控制權轉移至管線中下一個 Middleware 的回呼函數。
[Client Request] ──> [Middleware 1] ──> [Middleware 2] ──> [Route Handler] ──> [Client Response]

Middleware 的三大核心職責為:

  1. 執行任意程式碼(如記錄日誌、測量耗時)。
  2. 修改 reqres 物件(如驗證 Token 後掛載使用者資料)。
  3. 終結請求生命週期(直接呼叫 res.send()res.json()),或呼叫 next() 將流程往下傳遞。

常見的生態系 Middleware

在實際開發中,社群已針對多數通用情境提供了高度穩定且經過測試的開源中介軟體:

  • cors:處理跨來源資源共用(CORS),動態設定 Access-Control-Allow-Origin 等標頭。
  • helmet:透過自動設定適當的 HTTP 安全標頭(如 CSP、HSTS、X-Frame-Options)來提升應用安全性。
  • morgan:HTTP 請求記錄器(Logger),可用於終端機輸出與日誌檔分析。
  • express-rate-limit:限制短時間內的請求次數,防範暴力破解與 DDoS 攻擊。
  • **express.json() / express.urlencoded()**:Express 內建的 Body Parser,用於解析 JSON 與 URL-encoded 格式的請求主體。

如何自製客製化 Middleware

自製 Middleware 僅需定義一個接收 (req, res, next) 參數的函數,並確保在處理完畢後執行 next() 或終止回應。

1. 請求耗時記錄器(Request Logger)

// loggerMiddleware.js
const requestLogger = (req, res, next) => {
  const startTime = Date.now();
  const { method, originalUrl } = req;

  // 監聽 response 完成事件,計算處理總耗時
  res.on('finish', () => {
    const duration = Date.now() - startTime;
    console.log(`[${new Date().toISOString()}] ${method} ${originalUrl} ${res.statusCode} - ${duration}ms`);
  });

  next(); // 務必呼叫 next(),否則請求將卡住並超時
};

module.exports = requestLogger;

2. 身份驗證與授權防護(Auth Guard)

Middleware 常見的應用是在 req 物件上「擴充資訊」,供後續流程讀取:

// authMiddleware.js
const jwt = require('jsonwebtoken');

const authenticateToken = (req, res, next) => {
  const authHeader = req.headers['authorization'];
  const token = authHeader && authHeader.split(' ')[1]; // 格式: Bearer <TOKEN>

  if (!token) {
    return res.status(401).json({ error: '存取被拒:未提供 Token' });
  }

  jwt.verify(token, process.env.JWT_SECRET || 'secret_key', (err, user) => {
    if (err) {
      return res.status(403).json({ error: '憑證無效或已過期' });
    }
    
    // 將解碼後的用戶資訊掛載至 req,讓後續的 Controller 可直接讀取
    req.user = user;
    next();
  });
};

module.exports = authenticateToken;

Middleware 的註冊與套用層級

Express 提供了靈活的註冊機制,可依據業務需求將 Middleware 套用至全域、路由模組或單一端點。

1. 應用層級(Application-level)

使用 app.use() 註冊,所有進入應用的請求都會依宣告順序依序通過。

const express = require('express');
const cors = require('cors');
const helmet = require('helmet');
const requestLogger = require('./loggerMiddleware');

const app = express();

// 基礎全域 Middleware
app.use(helmet());
app.use(cors());
app.use(express.json());
app.use(requestLogger);

2. 路由模組層級(Router-level)

當系統架構擴大時,通常會使用 express.Router() 模組化各資源路由。此時可將 Middleware 綁定在特定的 Router 實例上。

// routes/userRoutes.js
const express = require('express');
const router = express.Router();
const authenticateToken = require('../authMiddleware');

// 該 Router 底下的所有子路由皆會自動經過 authenticateToken
router.use(authenticateToken);

router.get('/profile', (req, res) => {
  res.json({ message: '取得使用者資料成功', user: req.user });
});

router.put('/settings', (req, res) => {
  res.json({ message: '設定已更新' });
});

module.exports = router;

在主程式中掛載 Router:

const userRoutes = require('./routes/userRoutes');

// 僅有以 /api/v1/users 開頭的請求才會進入該 Router 及其內部的中介層
app.use('/api/v1/users', userRoutes);

3. 單一端點層級(Endpoint-level)

若某個 Middleware 僅適用於特定端點,可直接作為路由方法的第二參數傳入,支援傳入單一函式或陣列:

// 只有 POST /api/v1/posts 需要進行 authenticateToken 驗證
app.post('/api/v1/posts', authenticateToken, (req, res) => {
  res.status(201).json({ message: '文章發布成功', authorId: req.user.id });
});

錯誤處理中介軟體(Error-handling Middleware)

Express 透過函數簽名的參數數量來區分一般中介軟體與錯誤處理中介軟體。錯誤處理中介軟體必須嚴格宣告 4 個參數(err, req, res, next)

錯誤處理中介軟體必須放置在所有路由與 app.use() 宣告的最末端

// 全域錯誤捕捉器
app.use((err, req, res, next) => {
  console.error('[伺服器例外]', err.stack);

  const status = err.statusCode || 500;
  res.status(status).json({
    success: false,
    message: err.message || '內部伺服器錯誤',
    ...(process.env.NODE_ENV === 'development' && { stack: err.stack })
  });
});

當前方的任何 Middleware 或 Controller 執行 next(new Error('...')) 時,Express 會立即跳過管線中後續的所有一般 Middleware,直接進入此錯誤處理常式。


最佳實踐與注意事項

  • 順序決定一切:Middleware 的執行順序完全由 app.use() 註冊的先後順序決定。例如,Body Parser 必須宣告在任何需要讀取 req.body 的路由之前。
  • 避免管線中斷:自製 Middleware 內部若未發送回應(如 res.json()),務必在所有可能的分支路徑皆呼叫 next(),避免請求處於懸掛(Pending)狀態導致記憶體洩漏。
  • 非同步異常處理:在 Express 4.x 中,非同步函數(async/await)若拋出未捕獲的例外,需手動使用 try/catch 捕捉並透過 next(error) 轉發;在 Express 5.x 中則已原生支援自動捕捉 Promise Rejection。