Hono 生产实战 07:OpenAPI、RPC 和接口文档治理
wxk1991 Lv5

Hono 生产实战 07:OpenAPI、RPC 和接口文档治理

前后端分离项目里,接口文档往往有两种结局。

第一种是没人写。

第二种是写了但没人信。

比没有文档更糟糕的,是过期文档。

前端照着文档调接口,发现字段不对;后端说“你看代码吧”;测试同学只能打开浏览器抓包;新人入职第一周都在猜接口。

Hono 项目想做得稳,接口契约必须有治理。

这篇文章讲 Hono 里三种常见做法:

1
2
3
手写文档
OpenAPI
Hono RPC

重点不是“哪一个永远最好”,而是不同团队阶段应该怎么选。

技术栈快照时间:2026-06-23。Hono RPC、@hono/zod-openapi、Swagger UI 中间件会持续更新,生产使用前以官方文档为准。


一、接口文档的本质是契约

接口文档不是给领导看的。

它是前端、后端、测试、客户端、第三方接入方之间的契约。

一个好契约至少要回答:

1
2
3
4
5
6
7
8
9
请求路径是什么
请求方法是什么
需要哪些 header
path/query/body 参数是什么
响应结构是什么
错误码有哪些
鉴权方式是什么
字段是否可空
字段是否废弃

如果文档只写:

1
2
POST /api/user/create
创建用户

那它没有太大价值。

真正有价值的是结构化契约。


二、手写文档适合早期,但容易漂移

项目刚开始时,手写 Markdown 很快。

例如:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
## 创建文章

POST /api/posts

Request:

{
"title": "string",
"content": "string"
}

Response:

{
"data": {
"id": "string"
}
}

这种方式适合:

1
2
3
4
接口很少
团队很小
变化很快
还没有稳定契约

但它的问题也很明显:

1
2
3
4
5
代码改了文档不一定改
字段可空性很难表达
错误响应经常漏写
无法自动生成 SDK
无法自动做契约测试

所以手写文档可以作为起步方式,但不要长期依赖它承担生产契约。


三、OpenAPI 适合对外契约

OpenAPI 的好处是标准化。

它可以描述 HTTP API 的路径、参数、请求体、响应、错误、鉴权。

有了 OpenAPI,你可以做很多事情:

1
2
3
4
5
6
生成 Swagger UI
生成前端 SDK
生成客户端代码
生成 mock server
做契约测试
给第三方接入方文档

Hono 生态里常见做法是使用 @hono/zod-openapi

它把 Zod schema 和 OpenAPI route 定义结合起来。

这样你不用维护两套东西:

1
2
一套校验 schema
一套接口文档 schema

而是让 schema 同时服务于运行时校验和文档生成。


四、用 zod-openapi 定义接口

一个简单例子:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
import { OpenAPIHono, createRoute, z } from '@hono/zod-openapi'

const app = new OpenAPIHono()

const PostSchema = z.object({
id: z.string().openapi({
example: 'post_01JZ7Q5X4R8E9K7C2V3B',
}),
title: z.string().openapi({
example: 'Hono 接口设计实践',
}),
content: z.string(),
})

const ErrorSchema = z.object({
error: z.object({
code: z.string(),
message: z.string(),
}),
})

const createPostRoute = createRoute({
method: 'post',
path: '/api/posts',
request: {
body: {
content: {
'application/json': {
schema: z.object({
title: z.string().min(1),
content: z.string().min(1),
}),
},
},
},
},
responses: {
201: {
description: '创建成功',
content: {
'application/json': {
schema: z.object({
data: PostSchema,
}),
},
},
},
400: {
description: '请求参数错误',
content: {
'application/json': {
schema: ErrorSchema,
},
},
},
},
})

app.openapi(createPostRoute, async (c) => {
const input = c.req.valid('json')

const post = await createPost(input)

return c.json({
data: post,
}, 201)
})

这个例子里,最关键的是:

1
2
schema 不只用于文档
schema 也用于接口校验

接口实现和接口契约被放在同一个结构里,漂移概率会低很多。


五、暴露 OpenAPI JSON

定义完 route 后,可以暴露 OpenAPI 文档:

1
2
3
4
5
6
7
app.doc('/openapi.json', {
openapi: '3.0.0',
info: {
title: 'My Hono API',
version: '1.0.0',
},
})

再配合 Swagger UI:

1
2
3
4
5
import { swaggerUI } from '@hono/swagger-ui'

app.get('/docs', swaggerUI({
url: '/openapi.json',
}))

这样团队就有了一个可访问的接口文档页面。

生产环境建议做访问控制:

1
2
3
4
开发环境:直接开放 /docs
测试环境:登录后访问 /docs
生产环境:只对内网或管理账号开放
公开 API:提供专门的公开文档站点

不要把内部管理接口文档无脑暴露到公网。


六、错误响应也要进文档

很多团队只写成功响应。

这是不够的。

前端真正需要处理的是:

1
2
3
4
5
6
7
8
400 参数错误
401 未登录
403 无权限
404 不存在
409 状态冲突
422 业务校验失败
429 限流
500 服务错误

建议统一错误结构:

1
2
3
4
5
6
7
8
9
10
11
12
const ErrorResponseSchema = z.object({
error: z.object({
code: z.string().openapi({
example: 'POST_TITLE_REQUIRED',
}),
message: z.string().openapi({
example: '文章标题不能为空',
}),
details: z.unknown().optional(),
}),
requestId: z.string().optional(),
})

然后在每个 route 的 responses 里复用。

错误码也要有命名规范:

1
2
3
4
5
6
AUTH_REQUIRED
PERMISSION_DENIED
POST_NOT_FOUND
POST_STATUS_CONFLICT
PAYLOAD_TOO_LARGE
RATE_LIMITED

前端不能只靠中文 message 判断业务逻辑。

真正稳定的是 error code。


七、Hono RPC 适合内部前后端协作

Hono RPC 的思路和 OpenAPI 不一样。

OpenAPI 更像标准契约。

RPC 更像 TypeScript 项目之间共享类型能力。

如果你的前端和后端都在一个 monorepo 里,或者都使用 TypeScript,那么 Hono RPC 很舒服。

后端导出 app 类型:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
// apps/api/src/app.ts
import { Hono } from 'hono'

const app = new Hono()

const routes = app
.get('/api/posts/:id', async (c) => {
const id = c.req.param('id')

return c.json({
data: {
id,
title: 'Hono RPC 入门',
},
})
})
.post('/api/posts', async (c) => {
const input = await c.req.json()

return c.json({
data: {
id: crypto.randomUUID(),
...input,
},
}, 201)
})

export type AppType = typeof routes
export default app

前端使用 hc 创建客户端:

1
2
3
4
5
6
7
8
9
10
11
12
import { hc } from 'hono/client'
import type { AppType } from '@my-app/api'

const client = hc<AppType>('https://api.example.com')

const res = await client.api.posts[':id'].$get({
param: {
id: 'post_123',
},
})

const data = await res.json()

这样前端调用路径、参数结构、返回类型都能得到 TypeScript 提示。

对于内部项目,这个体验非常好。


八、RPC 不是公开 API 文档的替代品

RPC 很适合内部团队。

但如果你要给第三方开放 API,OpenAPI 仍然更合适。

原因很简单:

1
2
3
4
第三方不一定用 TypeScript
第三方不一定在你的 monorepo 里
第三方需要稳定版本和文档页面
第三方需要 SDK、示例、错误码、鉴权说明

所以我的建议是:

1
2
3
内部前后端:Hono RPC
公开 API:OpenAPI
需要同时支持:RPC 提升内部效率,OpenAPI 管理外部契约

不要把这两个东西对立起来。

它们解决的问题不同。


九、接口版本怎么设计

接口版本越早想清楚越好。

常见方式:

1
2
3
/api/v1/posts
/api/v2/posts
Accept: application/vnd.example.v1+json

大多数中小项目,路径版本最直观:

1
2
3
4
5
const v1 = new OpenAPIHono()

v1.openapi(createPostRoute, createPostHandler)

app.route('/api/v1', v1)

版本管理原则:

1
2
3
4
5
6
新增字段通常不升版本
删除字段必须升版本
改变字段语义必须升版本
改变错误码语义必须升版本
改变分页规则要谨慎
旧版本要有明确下线时间

不要今天改一个字段,明天告诉前端“你刷新一下类型”。

契约稳定,是协作效率的基础。


十、契约测试

有了 OpenAPI,不代表万事大吉。

你还要确保实际响应符合契约。

最低限度,要给关键接口写测试:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
import { describe, expect, it } from 'vitest'
import app from '../src/app'

describe('POST /api/posts', () => {
it('creates post', async () => {
const res = await app.request('/api/posts', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({
title: 'Hello',
content: 'World',
}),
})

expect(res.status).toBe(201)

const body = await res.json()
expect(body.data.id).toBeTypeOf('string')
expect(body.data.title).toBe('Hello')
})
})

如果有条件,可以用 OpenAPI schema 做更严格的响应校验。

但不要一开始追求复杂测试平台。

先把核心接口的成功和失败响应测起来。


十一、接口文档治理清单

一个生产 Hono 项目,我建议至少做到:

1
2
3
4
5
6
7
8
1. 所有 API 都有统一响应结构
2. 所有错误都有稳定 error code
3. 所有入参使用 schema 校验
4. 核心接口进入 OpenAPI 或 RPC 契约
5. 文档页面有访问控制
6. 公开 API 有版本号
7. breaking change 有迁移说明
8. 关键接口有契约测试

不要等接口数量超过 100 个再补文档。

那时候补文档不是治理,是考古。

Hono 很适合把契约写在代码旁边。

用好这一点,前后端协作会轻很多。