AI 电子伴侣
创建时间: 2026-04-08最后更新: 2026-04-18

1. 为什么需要数据校验

用户提交的数据永远不可信。少一个字段、类型不对、格式错误,这些情况每天都在发生。

手动校验长这样:

index.ts
01
app.post('/users', async (c) => {
02
const body = await c.req.json()
03
04
if (!body.name || typeof body.name !== 'string') {
05
return c.json({ error: 'name is required' }, 400)
06
}
07
if (!body.email || typeof body.email !== 'string') {
08
return c.json({ error: 'email is required' }, 400)
09
}
10
if (!body.email.includes('@')) {
11
return c.json({ error: 'invalid email' }, 400)
12
}
13
14
// 终于可以写业务逻辑了...
15
})

问题很明显:又丑又容易漏,字段一多就失控。而且校验完之后,TypeScript 依然不知道 body 的类型是什么,你还得手动断言。

Hono + Zod 的方案:用 schema 声明数据结构,校验和类型推导一步到位。

什么是 schema

如果你第一次接触后端校验,可以先把 schema 理解成一份数据规则说明书。它不是实际的数据,而是用来描述「什么样的数据才算合法」

比如下面这段数据:

data.json
1
{
2
"name": "Alice",
3
"email": "alice@example.com"
4
}

它对应的 schema 想表达的是:

  • name 这个字段必须存在
  • name 必须是字符串
  • email 这个字段必须存在
  • email 必须是合法邮箱格式

也就是说,schema 回答的不是「数据是什么」,而是「数据应该长什么样」

在没有 schema 的时候,这些规则通常散落在很多 if 判断里;有了 schema,这些规则就被集中写到一个地方,代码会更清楚。

用 Zod 写出来就是这样:

index.ts
1
const createUserSchema = z.object({
2
name: z.string().min(1),
3
email: z.string().email(),
4
})

这段 schema 可以直接读成自然语言:

  • 整体是一个对象 z.object({...})
  • name 是字符串,而且不能为空
  • email 是字符串,而且必须符合邮箱格式

所以后面你看到 schema 这个词时,不要把它想得太玄。你就把它当成一份「输入数据的规则定义」就行

2. 安装依赖

terminal
1
npm install zod @hono/zod-validator
  • zod:TypeScript-first 的数据校验库,定义 schema 的同时自动生成类型
  • @hono/zod-validator:Hono 官方的 Zod 集成,把 Zod schema 变成路由中间件

3. 基础用法

index.ts
01
import { Hono } from 'hono'
02
import { zValidator } from '@hono/zod-validator'
03
import { z } from 'zod'
04
05
const app = new Hono()
06
07
const createUserSchema = z.object({
08
name: z.string().min(1),
09
email: z.string().email(),
10
})
11
12
app.post('/users', zValidator('json', createUserSchema), (c) => {
13
const data = c.req.valid('json')
14
// data 的类型自动推导为 { name: string; email: string }
15
return c.json({ id: 1, ...data }, 201)
16
})
17
18
export default app

zValidator('json', schema) 做了两件事:

  1. 校验:请求体不符合 schema 时,中间件会直接拦截请求,默认返回一份 400 错误响应,请求根本不会进入你的处理函数
  2. 类型推导c.req.valid('json') 的返回类型由 schema 自动推导,不需要手写 interface

校验不通过时,默认返回的错误格式:

response.json
01
{
02
"success": false,
03
"error": {
04
"issues": [
05
{
06
"code": "too_small",
07
"minimum": 1,
08
"message": "String must contain at least 1 character(s)",
09
"path": ["name"]
10
}
11
]
12
}
13
}

4. 校验不同位置的数据

HTTP 请求的数据可能出现在不同位置,zValidator 的第一个参数指定校验哪里的数据:

校验请求体(JSON)

index.ts
1
const bodySchema = z.object({
2
title: z.string().min(1),
3
content: z.string(),
4
})
5
6
app.post('/posts', zValidator('json', bodySchema), (c) => {
7
const body = c.req.valid('json')
8
return c.json(body, 201)
9
})

这里有一个很容易踩的坑:如果你要校验 JSON,请求头里要正确带上 Content-Type: application/json。否则服务端可能根本不会按 JSON 的方式去解析请求体,最后你会觉得“明明传了数据,怎么校验不对”。

校验查询参数

index.ts
01
const querySchema = z.object({
02
page: z.coerce.number().int().positive().default(1),
03
limit: z.coerce.number().int().positive().default(10),
04
})
05
06
app.get('/posts', zValidator('query', querySchema), (c) => {
07
const { page, limit } = c.req.valid('query')
08
// page 和 limit 已经是 number 类型
09
return c.json({ page, limit })
10
})

注意:查询参数从 URL 解析出来本来都是字符串,这里用 z.coerce.number() 的意思是“先接收字符串,再自动帮你转成 number”。对新手来说,这种写法通常比 z.string().transform(Number) 更直观。

校验路由参数

index.ts
1
const paramSchema = z.object({
2
id: z.string().regex(/^\d+$/),
3
})
4
5
app.get('/users/:id', zValidator('param', paramSchema), (c) => {
6
const { id } = c.req.valid('param')
7
return c.json({ id })
8
})

校验请求头

index.ts
1
const headerSchema = z.object({
2
'x-api-key': z.string().min(1),
3
})
4
5
app.get('/admin/stats', zValidator('header', headerSchema), (c) => {
6
const headers = c.req.valid('header')
7
return c.json({ key: headers['x-api-key'] })
8
})

5. 自定义错误响应

默认的错误格式可能不符合你的 API 规范。zValidator 的第三个参数是一个 hook 函数,让你自定义错误响应:

index.ts
01
app.post(
02
'/users',
03
zValidator('json', createUserSchema, (result, c) => {
04
if (!result.success) {
05
return c.json(
06
{
07
code: 'VALIDATION_ERROR',
08
errors: result.error.flatten().fieldErrors,
09
},
10
400
11
)
12
}
13
}),
14
(c) => {
15
const data = c.req.valid('json')
16
return c.json({ id: 1, ...data }, 201)
17
}
18
)

result.error.flatten() 会把 Zod 的错误信息整理成更易读的格式:

response.json
1
{
2
"code": "VALIDATION_ERROR",
3
"errors": {
4
"name": ["String must contain at least 1 character(s)"],
5
"email": ["Invalid email"]
6
}
7
}

如果你的每个接口都需要统一的错误格式,可以封装一个工具函数:

这一段已经属于进阶写法了。如果你刚开始学,先会直接写 zValidator('json', schema) 就够了。下面这个封装的重点,不是让你背泛型,而是让你知道:同一种错误响应格式,是可以抽成工具复用的。

index.ts
01
import type { ValidationTargets } from 'hono'
02
import type { ZodSchema } from 'zod'
03
04
function validate<T extends keyof ValidationTargets>(target: T, schema: ZodSchema) {
05
return zValidator(target, schema, (result, c) => {
06
if (!result.success) {
07
return c.json(
08
{
09
code: 'VALIDATION_ERROR',
10
errors: result.error.flatten().fieldErrors,
11
},
12
400
13
)
14
}
15
})
16
}
17
18
// 使用
19
app.post('/users', validate('json', createUserSchema), (c) => {
20
const data = c.req.valid('json')
21
return c.json(data, 201)
22
})

6. 组合多个校验器

一个路由可以同时校验多个位置的数据,把多个 zValidator 当中间件依次挂上去就行:

index.ts
01
const paramSchema = z.object({
02
id: z.string().regex(/^\d+$/),
03
})
04
05
const bodySchema = z.object({
06
name: z.string().min(1),
07
email: z.string().email(),
08
})
09
10
const headerSchema = z.object({
11
'x-api-key': z.string().min(1),
12
})
13
14
app.put(
15
'/users/:id',
16
zValidator('param', paramSchema),
17
zValidator('json', bodySchema),
18
zValidator('header', headerSchema),
19
(c) => {
20
const { id } = c.req.valid('param')
21
const body = c.req.valid('json')
22
// 三个位置的数据都校验通过,类型都自动推导
23
return c.json({ id, ...body })
24
}
25
)

校验顺序就是中间件的排列顺序。第一个校验不通过,后面的不会执行。

7. 类型推导的价值

传统做法是手写一个 interface,然后在运行时另外写一套校验逻辑。两边要保持同步,改了一边忘了另一边就是 bug。

Zod 的方案是 schema 即类型。如果你需要在其他地方使用校验后的类型,用 z.infer 提取:

index.ts
01
const createUserSchema = z.object({
02
name: z.string().min(1),
03
email: z.string().email(),
04
age: z.number().int().min(0).optional(),
05
})
06
07
// 从 schema 推导出类型,不需要手写 interface
08
type CreateUserInput = z.infer<typeof createUserSchema>
09
// { name: string; email: string; age?: number | undefined }
10
11
function saveUser(input: CreateUserInput) {
12
// ...
13
}
14
15
app.post('/users', zValidator('json', createUserSchema), (c) => {
16
const data = c.req.valid('json')
17
saveUser(data) // 类型完全匹配,不需要任何转换
18
return c.json({ success: true }, 201)
19
})

一个 schema,同时解决校验和类型两个问题。改 schema 的时候 TypeScript 会自动提示所有受影响的地方。

8. 总结

zValidator 把数据校验从业务逻辑中剥离出来,变成声明式的中间件。schema 定义一次,校验和类型推导都有了。

核心用法就三个:

  • zValidator('json' | 'query' | 'param' | 'header', schema) — 校验指定位置的数据
  • 第三个参数 hook 函数 — 自定义错误响应
  • c.req.valid('json') — 拿到校验后的数据,类型自动推导

下一篇讲错误处理,看看怎么统一处理校验错误、业务错误和未知异常。