本文 AI 產出,尚未審核
ExpressJS (TypeScript) – Validation 請求驗證
使用 Zod / Yup 驗證 Schema
簡介
在 Web API 開發中,請求資料的驗證是不可或缺的安全與穩定基礎。若不對前端送來的參數進行嚴格檢查,容易導致資料庫錯誤、業務邏輯崩潰,甚至成為駭客攻擊的入口。
Express 搭配 TypeScript 已經提供了型別檢查的能力,但型別只在編譯期有效,執行時仍須自行驗證。這時候使用 Zod 或 Yup 這類宣告式驗證函式庫,就能在路由層快速、統一地定義 schema,同時保留完整的 TypeScript 型別推斷,讓程式碼既安全又易讀。
本文將從概念說明、實作範例、常見陷阱與最佳實踐,一步步帶你在 Express + TypeScript 專案中導入 Zod/Yup,完成完整的請求驗證流程。
核心概念
1. 為什麼選擇 Zod / Yup
| 特性 | Zod | Yup |
|---|---|---|
| 型別推斷 | 完全支援 TypeScript,編譯期即得到正確型別 | 需要 as 或 InferType 才能取得型別 |
| API 風格 | 函式式、鏈式呼叫、純函式 | 類似 Joi 的宣告式 |
| 效能 | 輕量、編譯時即生成驗證函式 | 相對較重,適合較複雜的條件驗證 |
| 社群與文件 | 官方文件完整,持續更新 | 仍在活躍維護,文件較分散 |
兩者皆支援 同步與非同步 的驗證流程,且可以與 Express 中間件輕鬆結合。選擇哪一個多半取決於團隊習慣與專案需求;以下範例同時示範兩者的寫法,讓你可以自行比較。
2. 基本使用流程
- 定義 Schema:描述請求 body、query、params 的結構與限制。
- 建立驗證中間件:將 Schema 包裝成 Express 中間件,於路由觸發前執行。
- 取得已驗證的資料:驗證成功時,將淨化後的資料掛到
req上,供後續處理使用。 - 錯誤處理:統一回傳錯誤訊息與 HTTP 狀態碼。
3. Zod 範例
3.1 安裝
npm i zod
npm i -D @types/express
3.2 定義 Schema
// src/schemas/userSchema.ts
import { z } from 'zod';
// 以 Zod 宣告使用者註冊的欄位
export const RegisterSchema = z.object({
username: z.string()
.min(3, { message: '帳號長度至少 3 個字元' })
.max(20, { message: '帳號長度不可超過 20 個字元' })
.regex(/^[a-zA-Z0-9_]+$/, { message: '只能使用英數與底線' }),
email: z.string()
.email({ message: '請輸入有效的 Email' }),
password: z.string()
.min(8, { message: '密碼長度至少 8 個字元' })
.regex(/[A-Z]/, { message: '必須包含大寫字母' })
.regex(/[0-9]/, { message: '必須包含數字' }),
// optional field with default value
role: z.enum(['user', 'admin']).default('user')
});
// 讓 TypeScript 自動推斷型別
export type RegisterInput = z.infer<typeof RegisterSchema>;
3.3 建立驗證中間件
// src/middleware/validate.ts
import { Request, Response, NextFunction } from 'express';
import { ZodSchema } from 'zod';
/**
* 產生一個可重複使用的 Zod 驗證中間件
* @param schema 欲驗證的 Zod schema
* @param property 要驗證的來源 (body / query / params)
*/
export const validate =
(schema: ZodSchema<any>, property: 'body' | 'query' | 'params' = 'body') =>
(req: Request, res: Response, next: NextFunction) => {
const result = schema.safeParse(req[property]);
if (!result.success) {
// 取得所有錯誤訊息
const errors = result.error.format();
return res.status(400).json({
message: '參數驗證失敗',
errors,
});
}
// 將淨化後的資料掛回 req,供後續使用
req[property] = result.data;
next();
};
3.4 套用於路由
// src/routes/auth.ts
import { Router } from 'express';
import { validate } from '../middleware/validate';
import { RegisterSchema, RegisterInput } from '../schemas/userSchema';
const router = Router();
/**
* POST /auth/register
* 使用 Zod 進行請求驗證
*/
router.post(
'/register',
validate(RegisterSchema, 'body'), // <-- 中間件
(req, res) => {
// 此時 TypeScript 已知 req.body 為 RegisterInput
const data: RegisterInput = req.body;
// TODO: 實作註冊邏輯 (hash password、寫入 DB 等)
res.status(201).json({ message: '註冊成功', user: data });
}
);
export default router;
重點:
validate中間件不僅會回傳錯誤訊息,還會把 淨化後的資料(已去除多餘屬性、轉型)寫回req,讓後續的控制器可以直接使用正確型別。
4. Yup 範例
4.1 安裝
npm i yup
npm i -D @types/express
4.2 定義 Schema
// src/schemas/productSchema.ts
import * as yup from 'yup';
export const CreateProductSchema = yup.object({
name: yup.string()
.required('商品名稱為必填')
.min(2, '名稱最少 2 個字元'),
price: yup.number()
.required('價格為必填')
.positive('價格必須大於 0'),
tags: yup.array()
.of(yup.string().max(15))
.default([]),
// 非同步驗證:檢查 SKU 是否已存在(假設有一個 DB 查詢函式)
sku: yup.string()
.required()
.test('unique-sku', 'SKU 已被使用', async (value) => {
// 假設有 async function checkSkuExists(sku: string): Promise<boolean>
const exists = await checkSkuExists(value!);
return !exists;
})
});
4.3 建立驗證中間件(支援 async)
// src/middleware/validateYup.ts
import { Request, Response, NextFunction } from 'express';
import { AnySchema } from 'yup';
export const validateYup =
(schema: AnySchema, property: 'body' | 'query' | 'params' = 'body') =>
async (req: Request, res: Response, next: NextFunction) => {
try {
const validated = await schema.validate(req[property], {
abortEarly: false, // 收集所有錯誤
stripUnknown: true, // 移除未在 schema 中的屬性
});
req[property] = validated;
next();
} catch (err: any) {
// Yup 會拋出 ValidationError
return res.status(400).json({
message: '參數驗證失敗',
errors: err.errors, // 陣列形式的錯誤訊息
});
}
};
4.4 套用於路由
// src/routes/product.ts
import { Router } from 'express';
import { validateYup } from '../middleware/validateYup';
import { CreateProductSchema } from '../schemas/productSchema';
const router = Router();
router.post(
'/',
validateYup(CreateProductSchema, 'body'), // <-- 中間件
(req, res) => {
const product = req.body; // 已經是 Yup 清理過的資料
// TODO: 寫入資料庫
res.status(201).json({ message: '商品建立成功', product });
}
);
export default router;
5. 常見陷阱與最佳實踐
| 陷阱 | 說明 | 解決方案 |
|---|---|---|
忘記在中間件回傳 next() |
中間件未呼叫 next() 會導致請求卡住 |
確保成功驗證後一定 return next(); |
混用 any 失去型別安全 |
直接使用 req.body as any 會失去 Zod/Yup 的好處 |
使用 z.infer<> 或 as const 取得正確型別 |
| 未處理非同步驗證錯誤 | Yup 的 test 需要 async/await,忽略會導致未捕獲的例外 |
在中間件使用 try/catch 或 safeParseAsync |
| 錯誤訊息過於冗長 | 直接回傳整個錯誤物件會暴露內部實作細節 | 只回傳 message 或自訂錯誤格式,避免資訊洩漏 |
| 驗證順序錯誤 | 先執行業務邏輯再驗證,會浪費資源 | 把驗證中間件放在路由最前面,確保「先驗證、後執行」 |
最佳實踐
- 統一錯誤格式:建立一個全局的錯誤處理器,將 Zod/Yup 的錯誤轉換成
{ code, field, message }的結構。 - 使用 TypeScript 型別推斷:盡量避免自行寫
interface,改用z.infer或yup.InferType,保持驗證與型別同步。 - 分層驗證:對於 query、params、body 分別建立獨立 Schema,避免一次性寫過長的物件。
- 設定
stripUnknown: true(Yup)或removeUnknownKeys: true(Zod):自動剔除多餘欄位,減少惡意資料注入。 - 重用 Schema:共用的欄位(如
id、page)可抽成獨立模組,降低維護成本。
6. 實際應用場景
| 場景 | 需求 | 建議方案 |
|---|---|---|
| 使用者註冊 | 必須檢查 Email 格式、密碼強度、帳號唯一性 | Zod + 自訂 async refine 檢查 DB,或 Yup 的 test |
| 分頁查詢 | page、limit 必須是正整數,且 limit 不超過 100 |
Zod z.coerce.number().int().min(1),z.preprocess 轉型 |
| 檔案上傳 + 其他欄位 | multipart/form-data 中的文字欄位需要驗證,檔案類型與大小也要檢查 |
先用 multer 處理檔案,再用 Zod/Yup 驗證 req.body,最後在路由內檢查 req.file |
| 第三方 API Callback | 收到的簽名與時間戳必須驗證,防止重放攻擊 | 使用 Zod z.object({ timestamp: z.string().refine(isRecent) }),結合自訂 refine |
| 多語系表單 | 前端會送 locale、translations 物件,必須保證每個語系都有必填欄位 |
Zod z.record(z.object({ title: z.string().min(1) })),或 Yup 的 object().shape({}).noUnknown() |
總結
- 請求驗證是 API 安全與健全的第一道防線,僅靠 TypeScript 型別不足以防止執行時的錯誤。
- Zod 提供 零成本的型別推斷 與 高效能的同步驗證,非常適合需要大量型別同步的專案。
- Yup 則在 非同步驗證(如資料庫唯一性檢查)上較為便利,且語法較貼近 Joi,適合已有 Yup 經驗的團隊。
- 透過 統一的驗證中間件,把所有驗證邏輯抽離到路由之外,保持控制器的乾淨與可測試性。
- 切記 錯誤訊息要友好且不洩漏內部實作,並在全局錯誤處理器中統一回傳格式,讓前端可以一致地呈現驗證結果。
掌握了 Zod/Yup 的基本用法與最佳實踐後,你就能在 Express + TypeScript 專案裡 快速、可靠地保護每一筆進來的資料,同時享受到 TypeScript 完整型別推斷所帶來的開發效率提升。祝你寫程式順利,API 安全無慮!