ExpressJS (TypeScript) – 建立 HTTP Server
主題:Request / Response 型別定義(Request、Response、NextFunction)
簡介
在使用 Express 建立 Web API 時,最常與開發者打交道的就是 req(Request)、res(Response) 以及 next(NextFunction) 三個物件。
如果在 JavaScript 中直接使用,它們會是 any,缺乏編譯時期的型別安全;但在 TypeScript 裡,我們可以透過官方提供的型別定義,讓 IDE 給予自動完成、錯誤提示,甚至在部署前就捕捉潛在的 BUG。
本篇文章將從 型別的基本概念、常見的使用情境、最佳實踐,一步步帶你掌握在 Express + TypeScript 專案中,如何正確、有效地使用 Request、Response、NextFunction。
核心概念
1. 為什麼要使用型別定義?
- 編譯時期檢查:錯誤會在
tsc階段就被捕捉,減少跑到執行環境才發現的問題。 - IDE 智慧提示:屬性、方法、泛型參數都有自動完成,開發效率提升。
- 文件自動生成:型別即是最好的文件,團隊成員不需要額外說明每個參數的結構。
Tip:即使是小型專案,養成使用型別的習慣,也能在未來擴充時減少重構成本。
2. Express 官方型別概覽
@types/express(隨 Express 4.x 以上自動安裝)提供了以下三個最常用的介面:
| 介面 | 位置 | 主要用途 |
|---|---|---|
Request |
express-serve-static-core |
代表 HTTP 請求,包含 params、query、body、headers 等 |
Response |
express-serve-static-core |
代表 HTTP 回應,提供 status、json、send、cookie 等方法 |
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'));
重點:即使不寫泛型,
Request、Response仍然提供完整的型別資訊,只是無法針對特定路由的參數做細部限制。
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.json的include或files路徑內,否則 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`,導致後續程式碼需要額外檢查。 |
最佳實踐清單
- 統一使用 TypeScript 泛型:
Request<Params, ResBody, ReqBody, Query>,讓每個路由都有專屬型別。 - 建立共用型別檔:
src/types/express.d.ts放置所有自訂屬性(如req.user),保持程式碼乾淨。 - 中介軟體必須回傳或呼叫
next:避免「請求永遠卡住」的情況。 - 錯誤處理器放在所有路由之後:確保任何
next(err)都能被捕獲。 - 使用
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。 - 實作:
- 建立
JwtPayload介面描述 token 內容。 - 使用
express-jwt或自行撰寫 middleware,將req.user設為JwtPayload。 - 在路由中直接使用
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' });
});
總結
Request、Response、NextFunction是 Express 中最核心的三個型別,正確使用可讓程式碼在編譯階段即捕捉錯誤,提升開發效率與程式品質。- 利用 泛型 為每條路由指定
params、query、body、response的型別,能讓 IDE 完全支援自動完成與型別檢查。 - 中介軟體 必須正確呼叫
next(),且錯誤處理器必須保留四個參數,才能保證錯誤流的正確傳遞。 - 當需要在
req上掛載自訂屬性(如user、locale)時,使用 declare module 方式擴充介面,避免 TypeScript 抱怨屬性不存在。 - 結合
async/await、express-async-handler、以及 統一的回傳型別,可以在大型專案中保持 API 的一致性與可維護性。
掌握上述概念後,你就能在 Express + TypeScript 的開發環境中,以型別安全的方式快速構建可靠、可擴充的 HTTP 伺服器。祝開發順利,寫出乾淨、可讀且安全的程式碼! 🚀