Hono 前后端分离系列 05:参数校验和统一错误响应
wxk1991 Lv5

Hono 前后端分离系列 05:参数校验和统一错误响应

后端接口不能相信前端。

这句话听起来有点冷,但它是前后端分离项目的底线。

前端表单可以校验,按钮可以禁用,页面可以限制输入。

但真正进入后端的请求,可能来自:

1
2
3
4
5
6
浏览器控制台
脚本
Postman
爬虫
错误版本的前端
恶意请求

所以 Hono API 必须做服务端校验。

而且不只是校验,还要把错误格式统一下来。否则前端处理错误会变成一堆 if else。


一、不要直接信任 c.req.json()

最危险的写法是这样:

1
2
3
4
5
6
7
8
9
10
app.post('/api/users', async (c) => {
const body = await c.req.json()

await createUser({
email: body.email,
name: body.name,
})

return c.json({ ok: true })
})

这段代码默认了很多事情:

1
2
3
4
5
body 一定是 JSON
email 一定存在
email 一定是字符串
name 一定合法
不会多传奇怪字段

这些默认都不可靠。

正确做法是先把输入变成可信数据,再进入业务逻辑。


二、使用 @hono/zod-validator

Hono 官方文档推荐可以使用 @hono/zod-validator 简化 Zod 校验。

安装:

1
pnpm add zod @hono/zod-validator

定义 schema:

1
2
3
4
5
6
import { z } from 'zod'

export const createUserSchema = z.object({
email: z.string().email(),
name: z.string().min(2).max(30),
})

在路由里使用:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
import { Hono } from 'hono'
import { zValidator } from '@hono/zod-validator'
import { createUserSchema } from '../schemas/user'

const users = new Hono()

users.post(
'/',
zValidator('json', createUserSchema),
async (c) => {
const input = c.req.valid('json')

return c.json({
data: {
id: 'u_1',
email: input.email,
name: input.name,
},
}, 201)
}
)

关键是这一行:

1
const input = c.req.valid('json')

它拿到的是已经校验过的数据。

业务逻辑应该从这里开始,而不是从裸 body 开始。


三、query 和 param 也要校验

很多人只校验 body,忽略 query 和 param。

分页接口:

1
GET /api/users?page=1&pageSize=20

也应该校验:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
const listUsersQuerySchema = z.object({
page: z.coerce.number().int().min(1).default(1),
pageSize: z.coerce.number().int().min(1).max(100).default(20),
keyword: z.string().trim().optional(),
})

users.get(
'/',
zValidator('query', listUsersQuerySchema),
async (c) => {
const query = c.req.valid('query')

return c.json({
data: {
items: [],
page: query.page,
pageSize: query.pageSize,
total: 0,
},
})
}
)

路径参数也一样:

1
2
3
4
5
6
7
8
9
10
11
12
const idParamSchema = z.object({
id: z.string().min(1),
})

users.get(
'/:id',
zValidator('param', idParamSchema),
async (c) => {
const { id } = c.req.valid('param')
return c.json({ data: { id } })
}
)

前后端分离项目里,参数来源越多,越要统一校验方式。


四、统一错误响应

校验失败时,不能让每个接口各自发挥。

推荐统一错误结构:

1
2
3
4
5
6
7
{
"error": {
"code": "VALIDATION_ERROR",
"message": "请求参数错误",
"details": []
}
}

可以封装一个错误响应工具:

1
2
3
4
5
6
7
8
9
10
11
12
13
export function errorResponse(
code: string,
message: string,
details?: unknown
) {
return {
error: {
code,
message,
details,
},
}
}

然后在 zValidator 里处理失败结果:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
users.post(
'/',
zValidator('json', createUserSchema, (result, c) => {
if (!result.success) {
return c.json(
errorResponse(
'VALIDATION_ERROR',
'请求参数错误',
result.error.flatten()
),
400
)
}
}),
async (c) => {
const input = c.req.valid('json')
return c.json({ data: input }, 201)
}
)

这样前端可以稳定处理:

1
2
3
4
if (!res.ok) {
const body = await res.json()
toast.error(body.error.message)
}

五、用 HTTPException 处理业务错误

校验错误是输入错误。

业务错误是另一类问题:

1
2
3
4
5
用户不存在
邮箱已注册
余额不足
没有权限
订单状态不允许取消

Hono 提供了 HTTPException,可以用来抛出 HTTP 语义明确的错误。

1
2
3
4
5
6
7
8
9
10
11
12
13
import { HTTPException } from 'hono/http-exception'

async function getUserOrThrow(id: string) {
const user = null

if (!user) {
throw new HTTPException(404, {
message: '用户不存在',
})
}

return user
}

再用 app.onError 统一兜底:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
app.onError((err, c) => {
if (err instanceof HTTPException) {
return c.json(
errorResponse('HTTP_ERROR', err.message),
err.status
)
}

console.error(err)

return c.json(
errorResponse('INTERNAL_SERVER_ERROR', '服务器内部错误'),
500
)
})

不要在每个 route 里写一遍 try catch。

统一错误处理才能让项目保持干净。


六、前端应该怎么配合

前端也要做表单校验,但目的不同。

前端校验是为了用户体验:

1
2
3
早点提示用户
减少无效请求
让表单更好用

后端校验是为了系统安全和数据正确。

两者不能互相替代。

如果前后端都用 TypeScript,可以把 schema 放到共享包里,或者通过 Hono RPC / OpenAPI 生成客户端类型。这样能减少重复定义。

但即使共享类型,后端仍然要做运行时校验。

TypeScript 类型只存在于编译期,请求进来时已经没有类型保护了。


七、最后的建议

参数校验和错误响应是 API 质量的地基。

我的建议是:

1
2
3
4
5
6
所有 body 都校验
重要 query 和 param 也校验
业务逻辑只接收校验后的 input
错误响应结构统一
业务错误用明确状态码
未知错误不要把堆栈返回给前端

一个接口能不能长期维护,往往不是看它成功时写得多漂亮,而是看它失败时是否可预测。

前后端分离最需要的就是可预测。


参考资料