Request Body 預設是什麼
HTTP request 的 body 是一串 bytes。Framework 把它解析成 object(JSON.parse / body-parser),但解析後的型別在多數框架裡是 any 或 Object——沒有任何保證說裡面有 name 欄位、email 格式正確、age 是數字。
// Express:req.body 是 any
app.post('/users', (req, res) => {
const { name, email, age } = req.body; // ← 全部 any,沒有驗證
// name 可能是 undefined
// email 可能是 "not-an-email"
// age 可能是 "twenty" (string 不是 number)
userService.create({ name, email, age });
});這個 any 就是後端大量 bug 的入口:null pointer、型別錯誤進資料庫、格式不對的資料被儲存。
資料綁定(Binding):把 raw request data 轉換成強型別的物件。
驗證(Validation):確認資料符合業務規則(必填、格式、範圍)。
Pydantic(FastAPI / Python)
FastAPI 的做法是最 elegant 的——你宣告型別,驗證自動發生:
from pydantic import BaseModel, EmailStr, Field
class UserCreateSchema(BaseModel):
name: str = Field(min_length=1, max_length=100)
email: EmailStr # ← 自動驗證 email 格式
age: int = Field(ge=0, le=150) # ← 必須是整數,0-150
@app.post("/users", status_code=201)
def create_user(user: UserCreateSchema): # ← 宣告型別就完成了
# user.name 是 str,已驗證
# user.email 是 validated email,已驗證
# user.age 是 int,已驗證
# 格式錯誤 → FastAPI 自動回 422 Unprocessable Entity
return user_service.create(user)FastAPI 看到 user: UserCreateSchema,自動:
- 從 request body 讀 JSON
- 用 Pydantic 解析成
UserCreateSchema實例 - 驗證失敗 → 直接回 422,帶詳細的錯誤資訊
- 驗證成功 →
user是完整的 typed object 傳進 handler
你不需要寫任何驗證邏輯,型別宣告即是驗證規則。
class-validator + Pipe(NestJS)
NestJS 的做法稍微明確一點,需要 DTO class + Pipe:
// create-user.dto.ts
import { IsEmail, IsString, IsInt, Min, Max, Length } from 'class-validator';
export class CreateUserDto {
@IsString()
@Length(1, 100)
name: string;
@IsEmail()
email: string;
@IsInt()
@Min(0)
@Max(150)
age: number;
}
// user.controller.ts
@Post()
create(@Body() dto: CreateUserDto) { // ← ValidationPipe 自動處理
return this.userService.create(dto);
// 驗證失敗 → NestJS 回 400 Bad Request
}
// main.ts — 全局開啟 ValidationPipe
app.useGlobalPipes(new ValidationPipe({ whitelist: true }));whitelist: true 很重要——它會把 DTO 裡沒有宣告的欄位自動刪掉(防止 mass assignment 攻擊,例如 client 送了 isAdmin: true 進來)。
@Valid + JSR-380(Spring Boot)
Java 的做法用標準的 Bean Validation(JSR-380):
// UserCreateDto.java
public class UserCreateDto {
@NotBlank
@Size(min = 1, max = 100)
private String name;
@Email
@NotBlank
private String email;
@NotNull
@Min(0)
@Max(150)
private Integer age;
// getters & setters...
}
// UserController.java
@PostMapping("/users")
@ResponseStatus(HttpStatus.CREATED)
public User create(@RequestBody @Valid UserCreateDto dto) {
// @Valid 觸發驗證
// 失敗 → Spring 回 400 Bad Request
return userService.create(dto);
}Java 的 annotation 和 Pydantic 的 Field() 在功能上類似,但語法更 verbose。@ControllerAdvice 可以捕獲 MethodArgumentNotValidException 統一格式化錯誤回應。
express-validator(Express)
Express 沒有內建驗證。express-validator 是主流選擇:
// validators/user.validator.ts
import { body, validationResult } from 'express-validator';
export const createUserValidators = [
body('name').isString().isLength({ min: 1, max: 100 }).trim(),
body('email').isEmail().normalizeEmail(),
body('age').isInt({ min: 0, max: 150 }),
];
// routes/users.ts
router.post('/users', createUserValidators, UserController.store);
// controllers/user.controller.ts(或 BaseController 統一處理)
static async store(req: Request, res: Response) {
const errors = validationResult(req);
if (!errors.isEmpty()) {
return res.status(400).json({ errors: errors.array() });
}
const { name, email, age } = req.body; // ← 仍然是 any,但已驗證過
await userService.create({ name, email, age });
}注意 Express 這個做法的缺點:req.body 驗證後仍然是 any,TypeScript 不知道它的型別。你要手動用 type assertion 或 zod parse() 讓它變成 typed object:
// 更強的做法:用 zod 同時做 parse + validate
import { z } from 'zod';
const UserCreateSchema = z.object({
name: z.string().min(1).max(100),
email: z.string().email(),
age: z.number().int().min(0).max(150),
});
type UserCreateDto = z.infer<typeof UserCreateSchema>; // ← 從 schema 推斷型別
static async store(req: Request, res: Response) {
const result = UserCreateSchema.safeParse(req.body);
if (!result.success) {
return res.status(400).json({ errors: result.error.flatten() });
}
const dto: UserCreateDto = result.data; // ← 現在是 typed
await userService.create(dto);
}各框架的 binding 發生點
不同框架在不同的地方做 binding:
| 框架 | binding 發生點 | 開發者感受 |
|---|---|---|
| FastAPI | function parameter 型別宣告時 | 看不到,自動發生 |
| NestJS | ValidationPipe + @Body() decorator | 需要設定 Pipe,之後自動 |
| Spring | @RequestBody @Valid | annotation 明確 |
| Express | 你呼叫 validationResult() 的時候 | 完全手動 |
FastAPI 和 NestJS 最省事:你宣告型別,框架做剩下的事。
Express 最 explicit:每一步都要你決定,但你也最清楚每一步在做什麼。
Express 的驗證實作
[[backend/framework/express/request-validation|[express][M6] Request Validation]] 有 proto 裡 express-validator + BaseController 的完整設計,說明為什麼把驗證結果檢查放在 BaseController 而不是每個 handler。
