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,导出类型
关键术语:Zod、safeParse、z.infer、flatten
示例 Prompt:
请用 Zod 创建用户注册的验证 schema:
- username: 必填,3-20个字符,只能包含字母数字下划线
- email: 必填,有效邮箱格式
- password: 必填,至少8位,包含大小写字母和数字
- confirmPassword: 必须与 password 相同
- age: 可选,18-100之间的整数
导出类型和验证函数避坑指南
- 查询参数都是字符串:使用
z.coerce转换类型 - parse vs safeParse:
parse会抛错,safeParse返回结果对象 - partial vs pick:
partial使所有字段可选,pick选择部分字段 - 错误信息要友好:提供清晰的中文提示
验收清单
- [ ] 所有 API 入口都有参数验证
- [ ] 验证 Schema 与 TypeScript 类型同步
- [ ] 错误响应包含清晰的字段级错误信息
- [ ] 验证逻辑可复用(抽离到独立文件)
