⚠️ Alpha内测版本警告:此为早期内部构建版本,尚不完整且可能存在错误,欢迎大家提Issue反馈问题或建议
Skip to content

3.6.2 非法请求怎么拦住——请求验证

一句话破题

永远不要信任客户端传来的数据——用 Zod 在入口处验证,把脏数据挡在门外。

核心价值

用户可能手滑、爬虫可能乱爬、攻击者可能恶意构造请求。参数验证是 API 的第一道防线,也是保证类型安全的关键。

为什么选择 Zod?

方案优点缺点
手动校验无依赖繁琐、不类型安全
JSON Schema标准化语法复杂、类型推导差
Zod类型推导、简洁、生态好需要额外依赖
tsx
// 手动校验(繁琐且不类型安全)
if (!body.title || typeof body.title !== 'string') {
  return Response.json({ error: '标题必填' }, { status: 400 })
}
if (body.title.length < 3) {
  return Response.json({ error: '标题至少3个字符' }, { status: 400 })
}
// ... 还有很多字段

// Zod 校验(简洁且类型安全)
const result = schema.safeParse(body)
if (!result.success) {
  return Response.json({ error: result.error }, { status: 400 })
}
// result.data 自动获得类型

Zod 基础

安装

bash
npm install zod

定义 Schema

tsx
import { z } from 'zod'

// 基础类型
const stringSchema = z.string()
const numberSchema = z.number()
const booleanSchema = z.boolean()

// 对象 Schema
const userSchema = z.object({
  name: z.string().min(2, '姓名至少2个字符'),
  email: z.string().email('邮箱格式不正确'),
  age: z.number().min(0).max(150).optional(),
})

// 从 Schema 推导 TypeScript 类型
type User = z.infer<typeof userSchema>
// { name: string; email: string; age?: number }

常用验证规则

字符串

tsx
z.string()
  .min(1, '不能为空')
  .max(100, '最多100个字符')
  .email('邮箱格式不正确')
  .url('URL格式不正确')
  .regex(/^[a-z]+$/, '只能包含小写字母')
  .trim() // 自动去除首尾空格
  .toLowerCase() // 自动转小写

数字

tsx
z.number()
  .min(0, '不能为负数')
  .max(100, '最大100')
  .int('必须是整数')
  .positive('必须是正数')

// 字符串转数字(常用于查询参数)
z.coerce.number()

数组

tsx
z.array(z.string())
  .min(1, '至少选择一项')
  .max(10, '最多选择10项')

枚举

tsx
z.enum(['pending', 'published', 'archived'])

// 或使用 union
z.union([z.literal('pending'), z.literal('published')])

可选与默认值

tsx
z.string().optional()           // string | undefined
z.string().nullable()           // string | null
z.string().default('untitled')  // 默认值

在 API Route 中使用

tsx
// lib/validations/post.ts
import { z } from 'zod'

export const createPostSchema = z.object({
  title: z.string().min(3, '标题至少3个字符').max(100),
  content: z.string().min(10, '内容至少10个字符'),
  published: z.boolean().default(false),
  tags: z.array(z.string()).optional(),
})

export const updatePostSchema = createPostSchema.partial()

export const queryPostsSchema = z.object({
  page: z.coerce.number().min(1).default(1),
  limit: z.coerce.number().min(1).max(100).default(10),
  status: z.enum(['draft', 'published']).optional(),
})

export type CreatePostInput = z.infer<typeof createPostSchema>
tsx
// app/api/posts/route.ts
import { createPostSchema, queryPostsSchema } from '@/lib/validations/post'

export async function GET(request: Request) {
  const { searchParams } = new URL(request.url)
  const query = Object.fromEntries(searchParams)
  
  const result = queryPostsSchema.safeParse(query)
  if (!result.success) {
    return Response.json(
      { error: result.error.flatten() },
      { status: 400 }
    )
  }
  
  const { page, limit, status } = result.data
  // 现在 page 和 limit 都是 number 类型了
}

export async function POST(request: Request) {
  const body = await request.json()
  
  const result = createPostSchema.safeParse(body)
  if (!result.success) {
    return Response.json(
      { error: result.error.flatten() },
      { status: 400 }
    )
  }
  
  // result.data 类型是 CreatePostInput
  const post = await postService.create(result.data)
  return Response.json(post, { status: 201 })
}

错误信息格式化

tsx
const result = schema.safeParse(data)

if (!result.success) {
  // 方式一:扁平化错误
  const errors = result.error.flatten()
  // {
  //   fieldErrors: { title: ['标题太短'], email: ['邮箱格式不正确'] },
  //   formErrors: []
  // }
  
  // 方式二:格式化错误
  const formatted = result.error.format()
  // {
  //   title: { _errors: ['标题太短'] },
  //   email: { _errors: ['邮箱格式不正确'] }
  // }
  
  // 方式三:所有错误
  const issues = result.error.issues
  // [{ path: ['title'], message: '标题太短' }, ...]
}

封装验证函数

tsx
// lib/validate.ts
import { z, ZodSchema } from 'zod'

export function validate<T>(schema: ZodSchema<T>, data: unknown): 
  | { success: true; data: T }
  | { success: false; error: Response } {
  
  const result = schema.safeParse(data)
  
  if (!result.success) {
    return {
      success: false,
      error: Response.json(
        { error: { code: 'VALIDATION_ERROR', details: result.error.flatten() } },
        { status: 400 }
      ),
    }
  }
  
  return { success: true, data: result.data }
}

// 使用
export async function POST(request: Request) {
  const body = await request.json()
  const validation = validate(createPostSchema, body)
  
  if (!validation.success) {
    return validation.error
  }
  
  // validation.data 是类型安全的
}

AI 协作指南

核心意图:让 AI 生成类型安全的参数验证。

需求定义公式

  • 功能描述:验证 [资源] 的创建/更新请求
  • 字段要求:[字段名]: [类型], [验证规则]
  • 技术要求:使用 Zod,导出类型

关键术语ZodsafeParsez.inferflatten

示例 Prompt

请用 Zod 创建用户注册的验证 schema:
- username: 必填,3-20个字符,只能包含字母数字下划线
- email: 必填,有效邮箱格式
- password: 必填,至少8位,包含大小写字母和数字
- confirmPassword: 必须与 password 相同
- age: 可选,18-100之间的整数
导出类型和验证函数

避坑指南

  1. 查询参数都是字符串:使用 z.coerce 转换类型
  2. parse vs safeParseparse 会抛错,safeParse 返回结果对象
  3. partial vs pickpartial 使所有字段可选,pick 选择部分字段
  4. 错误信息要友好:提供清晰的中文提示

验收清单

  • [ ] 所有 API 入口都有参数验证
  • [ ] 验证 Schema 与 TypeScript 类型同步
  • [ ] 错误响应包含清晰的字段级错误信息
  • [ ] 验证逻辑可复用(抽离到独立文件)