Hono 前后端分离系列 09:测试、日志和可观测性
能跑起来的 API,不等于能上线。
能上线的 API,也不等于能维护。
前后端分离项目一旦进入真实使用,就会出现这些问题:
1 2 3 4 5
| 前端说接口偶尔 500 用户说提交后没反应 日志里看不出是哪次请求 本地无法复现线上问题 改了一个接口,另一个页面坏了
|
这时你需要的不只是代码,而是测试、日志和可观测性。
Hono 本身很轻,但它提供的 app.request()、Testing Helper、中间件机制,很适合把这些基础设施补起来。
一、先测试 route 行为
Hono 应用可以直接通过 app.request() 测试。
1 2 3 4 5 6 7 8 9 10 11 12 13
| import { describe, expect, it } from 'vitest' import app from '../src/app'
describe('health', () => { it('returns ok', async () => { const res = await app.request('/api/health')
expect(res.status).toBe(200) expect(await res.json()).toEqual({ ok: true, }) }) })
|
这类测试很适合覆盖:
- 状态码
- 响应结构
- 参数校验
- 鉴权失败
- 权限不足
- 常见业务错误
不要只测成功路径。
失败路径才是 API 稳定性的关键。
二、使用 testClient 做类型安全测试
Hono 还提供 Testing Helper。
官方文档里提到,testClient() 可以根据 Hono routes 返回类型化客户端,类似 Hono Client。
示例:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18
| import { testClient } from 'hono/testing' import { describe, expect, it } from 'vitest' import app from '../src/app'
const client = testClient(app)
describe('users', () => { it('lists users', async () => { const res = await client.api.users.$get({ query: { page: '1', pageSize: '20', }, })
expect(res.status).toBe(200) }) })
|
需要注意的是,Hono 文档提醒,类型推断依赖 route 链式定义方式。也就是说,如果你希望测试客户端获得完整类型提示,路由组织方式要配合。
这点在项目早期就要想好。
三、测试错误响应
前后端分离项目里,错误响应必须稳定。
比如参数错误:
1 2 3 4 5 6 7 8 9 10 11 12 13
| it('returns validation error', async () => { const res = await client.api.users.$post({ json: { email: 'bad-email', name: '', }, })
expect(res.status).toBe(400)
const body = await res.json() expect(body.error.code).toBe('VALIDATION_ERROR') })
|
比如未登录:
1 2 3 4
| it('returns 401 without token', async () => { const res = await app.request('/api/private/me') expect(res.status).toBe(401) })
|
比如无权限:
1 2 3 4 5 6 7 8 9
| it('returns 403 for normal user', async () => { const res = await app.request('/api/admin/users', { headers: { Authorization: `Bearer ${normalUserToken}`, }, })
expect(res.status).toBe(403) })
|
这些测试比“接口能返回 200”更有价值。
四、日志要带 requestId
线上排查问题时,最怕日志散。
一个请求经过多个中间件、多个 service、多个数据库操作,如果没有 requestId,你很难串起来。
Hono 提供了 Request ID middleware。
你也可以自己写一个简单版本:
1 2 3 4 5 6 7 8 9
| app.use('*', async (c, next) => { const requestId = c.req.header('X-Request-Id') ?? crypto.randomUUID()
c.set('requestId', requestId) c.header('X-Request-Id', requestId)
await next() })
|
错误响应里也带上:
1 2 3 4 5 6 7
| return c.json({ error: { code: 'INTERNAL_SERVER_ERROR', message: '服务器内部错误', }, requestId: c.get('requestId'), }, 500)
|
这样前端报错时,可以直接把 requestId 打出来。
1 2 3
| console.error('API failed', { requestId: body.requestId, })
|
排查效率会高很多。
五、日志不要只打印字符串
不要只写:
1
| console.log('create user failed')
|
至少要结构化:
1 2 3 4 5
| console.error('create_user_failed', { requestId: c.get('requestId'), userId: c.get('user')?.id, error: err instanceof Error ? err.message : String(err), })
|
如果部署在 Cloudflare Workers,console.log 会进入平台日志。
如果部署在 Node.js,可以接 Pino、Winston 或其他日志系统。
关键不是用哪个库,而是日志字段要稳定:
1 2 3 4 5 6 7
| requestId userId route method status duration error.code
|
六、记录耗时
Hono 有 Timing middleware,可以给响应加 Server-Timing 信息。
你也可以写一个基础耗时日志:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15
| app.use('*', async (c, next) => { const startedAt = Date.now()
await next()
const duration = Date.now() - startedAt
console.log('request_finished', { method: c.req.method, path: new URL(c.req.url).pathname, status: c.res.status, duration, requestId: c.get('requestId'), }) })
|
慢接口要能被发现。
不要等用户说“页面很卡”,你才开始猜。
七、onError 是最后防线
Hono 的 app.onError() 很适合做统一兜底。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16
| app.onError((err, c) => { const requestId = c.get('requestId')
console.error('unhandled_error', { requestId, error: err instanceof Error ? err.stack : String(err), })
return c.json({ error: { code: 'INTERNAL_SERVER_ERROR', message: '服务器内部错误', }, requestId, }, 500) })
|
生产环境不要把 stack 直接返回给前端。
前端只需要知道请求失败和 requestId。
堆栈应该留在服务端日志里。
八、最后的建议
Hono API 要能长期维护,至少做到:
1 2 3 4 5 6 7
| 核心接口有测试 错误路径有测试 每次请求有 requestId 错误响应带 requestId 日志结构化 慢请求能看见 onError 统一兜底
|
前后端分离项目里,前端报错截图经常只是一条线索。
真正能定位问题的,是后端日志、请求 ID、状态码、错误码和测试覆盖。
不要等线上出问题才补这些。
参考资料