到这里,Hono 已经像一个很顺手的边缘 API 框架了。
但它真正让很多 TS 团队觉得“这玩意儿有意思”的地方,是 RPC 类型联动。
你可以把它想成:
后方参谋部和前线轻骑,不是各画一张地图,而是共用同一张作战图。
#服务端
// server.ts
import { Hono } from 'hono'
import { z } from 'zod'
import { zValidator } from '@hono/zod-validator'
const createDispatchSchema = z.object({
outpostId: z.string(),
supply: z.string(),
})
export const app = new Hono()
.post(
'/dispatches',
zValidator('json', createDispatchSchema),
(c) => {
const input = c.req.valid('json')
return c.json(
{
id: 'dispatch-001',
...input,
status: 'queued',
},
201
)
}
)
.get('/dispatches/:id', (c) => {
const id = c.req.param('id')
if (id !== 'dispatch-001') {
return c.json({ error: 'not found' }, 404)
}
return c.json(
{
id,
status: 'queued',
},
200
)
})
export type AppType = typeof app#客户端
// client.ts
import { hc } from 'hono/client'
import type { AppType } from './server'
const client = hc<AppType>('https://api.software-city.example', {
headers: {
Authorization: 'Bearer TOKEN',
},
})
const created = await client.dispatches.$post({
json: {
outpostId: 'north-1',
supply: 'medkit',
},
})
const detail = await client.dispatches[':id'].$get({
param: {
id: 'dispatch-001',
},
})
if (detail.status === 200) {
const data = await detail.json()
console.log(data.status)
}#这里爽在哪里
爽点不是“少写几个接口文档”,而是:
- 路径结构是有类型的
param / query / json / form这些输入是有类型的c.json(..., 200)、c.json(..., 404)这些响应分支也能带类型
也就是说,类型图谱从服务端延伸到了客户端。
#一个 RPC 重要细节:状态码尽量显式
官方文档特别强调过,如果你想让客户端推断足够准,最好显式写:
return c.json({ error: 'not found' }, 404)
return c.json({ post }, 200)而不是全靠默认行为。
尤其是 404 这种分支,如果你直接 c.notFound(),默认情况下客户端不一定能得到理想推断。要么显式 c.json(..., 404),要么你额外扩展类型。
#Monorepo 细节
如果你在 monorepo 里共享 AppType,官方 RPC 文档也明确提醒:
客户端和服务端的 tsconfig.json 都要开 strict: true。
否则你会以为地图共用了,实际上很多类型信息没真正穿过去。