本文 AI 產出,尚未審核

ExpressJS (TypeScript) – 建立 HTTP Server

主題:Request / Response 型別定義(RequestResponseNextFunction


簡介

在使用 Express 建立 Web API 時,最常與開發者打交道的就是 req(Request)res(Response) 以及 next(NextFunction) 三個物件。
如果在 JavaScript 中直接使用,它們會是 any,缺乏編譯時期的型別安全;但在 TypeScript 裡,我們可以透過官方提供的型別定義,讓 IDE 給予自動完成、錯誤提示,甚至在部署前就捕捉潛在的 BUG。

本篇文章將從 型別的基本概念常見的使用情境最佳實踐,一步步帶你掌握在 Express + TypeScript 專案中,如何正確、有效地使用 RequestResponseNextFunction


核心概念

1. 為什麼要使用型別定義?

  • 編譯時期檢查:錯誤會在 tsc 階段就被捕捉,減少跑到執行環境才發現的問題。
  • IDE 智慧提示:屬性、方法、泛型參數都有自動完成,開發效率提升。
  • 文件自動生成:型別即是最好的文件,團隊成員不需要額外說明每個參數的結構。

Tip:即使是小型專案,養成使用型別的習慣,也能在未來擴充時減少重構成本。

2. Express 官方型別概覽

@types/express(隨 Express 4.x 以上自動安裝)提供了以下三個最常用的介面:

介面 位置 主要用途
Request express-serve-static-core 代表 HTTP 請求,包含 paramsquerybodyheaders
Response express-serve-static-core 代表 HTTP 回應,提供 statusjsonsendcookie 等方法
NextFunction express-serve-static-core 中介軟體(middleware)中傳遞控制權的函式

這三個介面都支援 泛型,讓我們可以在宣告時就指定 路由參數查詢字串請求主體 的型別。

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

/**
 * 泛型說明:
 * Request<P, ResBody, ReqBody, ReqQuery>
 *   P        → URL 路徑參數 (e.g. /users/:id)
 *   ResBody  → 回傳資料的型別(Response.json 時的型別)
 *   ReqBody  → 請求主體的型別(req.body)
 *   ReqQuery → 查詢字串的型別(req.query)
 */

3. 基礎範例:型別化的 Hello World

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

const app = express();
app.use(express.json()); // 解析 JSON body

// 沒有使用泛型的寫法(會失去型別安全)
// app.get('/hello', (req, res) => { ... });

app.get('/hello', (req: Request, res: Response) => {
  res.send('Hello, Express + TypeScript!');
});

app.listen(3000, () => console.log('Server running on http://localhost:3000'));

重點:即使不寫泛型,RequestResponse 仍然提供完整的型別資訊,只是無法針對特定路由的參數做細部限制

4. 使用泛型定義路由參數、查詢字串與請求主體

// 定義型別
interface UserParams {
  id: string;               // /users/:id
}
interface UserQuery {
  expand?: 'profile' | 'settings';
}
interface CreateUserBody {
  name: string;
  email: string;
  age?: number;
}

// GET /users/:id?expand=profile
app.get(
  '/users/:id',
  (req: Request<UserParams, any, any, UserQuery>, res: Response) => {
    const userId = req.params.id;          // 型別為 string
    const expand = req.query.expand;       // 型別為 'profile' | 'settings' | undefined
    // 依據 expand 做不同的資料取得...
    res.json({ userId, expand });
  }
);

// POST /users
app.post(
  '/users',
  (req: Request<{}, any, CreateUserBody>, res: Response) => {
    const { name, email, age } = req.body; // 完全根據 CreateUserBody 推斷型別
    // 進行資料庫寫入...
    res.status(201).json({ message: 'User created', user: { name, email, age } });
  }
);

為什麼要把 any 放在 ResBody

  • ResBody 只會影響 res.json() 的型別推斷。若不需要在同一個 handler 中使用 res.json 的回傳型別,直接寫 any 最簡潔。若想要更嚴格的回傳型別,可自行定義。

5. 中介軟體(Middleware)與 NextFunction

NextFunction 代表「把控制權交給下一個 middleware」的函式。若在中介軟體裡拋出錯誤或呼叫 next(err),Express 會自動轉交給錯誤處理器。

// 自訂驗證中介軟體
import { Request, Response, NextFunction } from 'express';

function requireAuth(
  req: Request,
  res: Response,
  next: NextFunction
): void {
  const authHeader = req.headers.authorization;
  if (!authHeader) {
    // 直接回傳 401,結束流程
    res.status(401).json({ error: 'Unauthorized' });
    return;
  }

  // 假設 token 為 "Bearer <token>"
  const token = authHeader.split(' ')[1];
  if (token !== 'valid-token') {
    // 交給錯誤處理 middleware
    const err = new Error('Invalid token');
    // @ts-ignore: Express 的錯誤型別為 any,這裡直接傳錯誤物件
    next(err);
    return;
  }

  // 驗證通過,繼續往下走
  next();
}

// 套用於路由
app.get('/protected', requireAuth, (req, res) => {
  res.json({ secret: 'You can see this because you are authenticated!' });
});

NextFunction 的型別特性

  • 簽名(err?: any) => void
  • 若傳入 err,Express 會跳過後續的普通 middleware,直接跑錯誤處理器 (app.use((err, req, res, next) => {...}))。

6. 錯誤處理器的型別

app.use(
  (err: Error, req: Request, res: Response, _next: NextFunction) => {
    console.error('Error:', err.message);
    res.status(500).json({ error: err.message });
  }
);
  • 第四個參數 必須 命名為 next(或 _next),否則 TypeScript 會把此函式當成普通 middleware,導致跑時錯誤。

7. 進階:自訂 Request 介面(擴充屬性)

有時候會在 middleware 裡把使用者資訊掛到 req 上,這時需要 擴充 Request 介面,避免 Property 'user' does not exist 的編譯錯誤。

// types/express.d.ts
import 'express-serve-static-core';

declare module 'express-serve-static-core' {
  interface Request {
    user?: {
      id: string;
      role: 'admin' | 'member';
    };
  }
}

// 在驗證成功後掛載 user
function attachUser(req: Request, _res: Response, next: NextFunction) {
  req.user = { id: '123', role: 'admin' };
  next();
}

// 後續路由可直接使用 req.user
app.get('/profile', attachUser, (req, res) => {
  if (!req.user) {
    return res.status(401).json({ error: 'No user attached' });
  }
  res.json({ profile: `User ID: ${req.user.id}, Role: ${req.user.role}` });
});

注意:自訂型別檔案需放在 tsconfig.jsonincludefiles 路徑內,否則 TypeScript 不會自動載入。


常見陷阱與最佳實踐

陷阱 說明 解決方式 / 最佳實踐
忘記使用 express.json() req.body 會是 undefined,導致型別錯誤。 app.use(express.json()) 之後才使用 req.body
直接使用 any 失去型別安全,等同於純 JavaScript。 儘量使用 泛型,或自行定義介面。
NextFunction 被遺漏 中介軟體未正確傳遞控制權,請求會卡住。 中介軟體簽名一定要包含 next: NextFunction,且在所有分支路徑都呼叫 next()(或回傳 response)。
錯誤處理器缺少四個參數 Express 會把它當成普通 middleware,錯誤不會被捕捉。 必須寫成 (err, req, res, next) => {},即使不使用 next 也要保留參數。
擴充 Request 時未正確 declare module TypeScript 仍報錯 Property does not exist *.d.ts 檔案裡 declare module 'express-serve-static-core',並 重啟編譯器
使用 req.params 時忘記定義型別 會得到 `string undefined`,導致後續程式碼需要額外檢查。

最佳實踐清單

  1. 統一使用 TypeScript 泛型Request<Params, ResBody, ReqBody, Query>,讓每個路由都有專屬型別。
  2. 建立共用型別檔src/types/express.d.ts 放置所有自訂屬性(如 req.user),保持程式碼乾淨。
  3. 中介軟體必須回傳或呼叫 next:避免「請求永遠卡住」的情況。
  4. 錯誤處理器放在所有路由之後:確保任何 next(err) 都能被捕獲。
  5. 使用 async/await + express-async-handler:讓異步錯誤自動傳給錯誤處理器,減少手動 try/catch
import asyncHandler from 'express-async-handler';

app.get(
  '/async-data',
  asyncHandler(async (req: Request, res: Response) => {
    const data = await fetchData(); // 若拋錯會自動交給錯誤處理器
    res.json(data);
  })
);

實際應用場景

1. 企業級 API:分層驗證與授權

  • 需求:所有受保護的路由必須先驗證 JWT,並把使用者資訊掛到 req.user
  • 實作
    1. 建立 JwtPayload 介面描述 token 內容。
    2. 使用 express-jwt 或自行撰寫 middleware,將 req.user 設為 JwtPayload
    3. 在路由中直接使用 req.user!.role 判斷權限。
interface JwtPayload {
  sub: string;      // user id
  role: 'admin' | 'member';
  iat: number;
  exp: number;
}

// middleware
function verifyJwt(req: Request, _res: Response, next: NextFunction) {
  const token = req.headers.authorization?.split(' ')[1];
  if (!token) return res.status(401).json({ error: 'Missing token' });

  try {
    const payload = jwt.verify(token, process.env.JWT_SECRET) as JwtPayload;
    req.user = { id: payload.sub, role: payload.role };
    next();
  } catch (e) {
    next(e);
  }
}

// route
app.get('/admin/dashboard', verifyJwt, (req, res) => {
  if (req.user?.role !== 'admin') {
    return res.status(403).json({ error: 'Forbidden' });
  }
  res.json({ secretData: 'Only admin can see this' });
});

2. 大型前端與後端分離專案:統一 API 回傳型別

  • 需求:前端使用 Swagger UI,後端需要保證每個 endpoint 的回傳結構一致。
  • 做法:為每個路由定義 ResBody 泛型,讓 res.json() 的型別即時檢查。
interface Paginated<T> {
  items: T[];
  total: number;
  page: number;
  limit: number;
}

interface User {
  id: string;
  name: string;
  email: string;
}

// GET /users?page=1&limit=10
app.get(
  '/users',
  (req: Request<{}, Paginated<User>, {}, { page?: string; limit?: string }>, res) => {
    const page = Number(req.query.page) || 1;
    const limit = Number(req.query.limit) || 10;
    const data: Paginated<User> = {
      items: [], // 取自 DB
      total: 0,
      page,
      limit,
    };
    res.json(data); // TypeScript 確保 data 符合 Paginated<User>
  }
);

3. 低延遲微服務:使用 NextFunction 進行串流處理

在需要 分段處理(如驗證 → 記錄 → 真正業務)時,可將每個步驟寫成獨立 middleware,讓程式碼更易維護。

// logger middleware
function logger(req: Request, _res: Response, next: NextFunction) {
  console.log(`[${new Date().toISOString()}] ${req.method} ${req.path}`);
  next(); // 交給下一個 middleware
}

// validation middleware
function validateCreate(req: Request<{}, {}, CreateUserBody>, _res: Response, next: NextFunction) {
  const { name, email } = req.body;
  if (!name || !email) {
    return _res.status(400).json({ error: 'Name & Email required' });
  }
  next();
}

// 真正的業務邏輯
app.post('/users', logger, validateCreate, async (req, res) => {
  // 省略 DB 操作...
  res.status(201).json({ message: 'Created' });
});

總結

  • RequestResponseNextFunction 是 Express 中最核心的三個型別,正確使用可讓程式碼在編譯階段即捕捉錯誤,提升開發效率與程式品質。
  • 利用 泛型 為每條路由指定 paramsquerybodyresponse 的型別,能讓 IDE 完全支援自動完成與型別檢查。
  • 中介軟體 必須正確呼叫 next(),且錯誤處理器必須保留四個參數,才能保證錯誤流的正確傳遞。
  • 當需要在 req 上掛載自訂屬性(如 userlocale)時,使用 declare module 方式擴充介面,避免 TypeScript 抱怨屬性不存在。
  • 結合 async/awaitexpress-async-handler、以及 統一的回傳型別,可以在大型專案中保持 API 的一致性與可維護性。

掌握上述概念後,你就能在 Express + TypeScript 的開發環境中,以型別安全的方式快速構建可靠、可擴充的 HTTP 伺服器。祝開發順利,寫出乾淨、可讀且安全的程式碼! 🚀