Request Body 預設是什麼

HTTP request 的 body 是一串 bytes。Framework 把它解析成 object(JSON.parse / body-parser),但解析後的型別在多數框架裡是 anyObject——沒有任何保證說裡面有 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,自動:

  1. 從 request body 讀 JSON
  2. 用 Pydantic 解析成 UserCreateSchema 實例
  3. 驗證失敗 → 直接回 422,帶詳細的錯誤資訊
  4. 驗證成功 → 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 發生點開發者感受
FastAPIfunction parameter 型別宣告時看不到,自動發生
NestJSValidationPipe + @Body() decorator需要設定 Pipe,之後自動
Spring@RequestBody @Validannotation 明確
Express你呼叫 validationResult() 的時候完全手動

FastAPI 和 NestJS 最省事:你宣告型別,框架做剩下的事。
Express 最 explicit:每一步都要你決定,但你也最清楚每一步在做什麼。


Express 的驗證實作

[[backend/framework/express/request-validation|[express][M6] Request Validation]] 有 proto 裡 express-validator + BaseController 的完整設計,說明為什麼把驗證結果檢查放在 BaseController 而不是每個 handler。