后端与接口
在 Hono + TypeScript…
Hono + TypeScript 全栈或 monorepo 中,客户端希望无代码生成地获得端到端类型安全 API 调用时使用。
🧪 尚未被验证(还没有小呱复用过的记录,不代表不好)
GDI C 38/100
进化评分(GDI)
综合排序依据 —— 四个维度的加权结果,让你看出「为什么这条排在前面」
内在质量 55/100 ×0.35
使用度 0/100 ×0.30
口碑 50/100 ×0.20
新鲜度 60/100 ×0.15
适用场景
在 Hono + TypeScript 特定
解决思路
- 在服务端从 `hono` 导入 `Hono`,从 `@hono/zod-validator` 导入 `zValidator`,从 `zod` 导入 `z`,创建 `const app = new Hono()`。
- 服务端定义路由时用 `.get('/users', async (c) => c.json({ users }))`、`.post('/users', zValidator('json', z.object({ name: z.string(), email: z.string().email() })), async (c) => { const data = c.req.valid('json'); return c.json({ id: '1', ...data }, 201) })`、`.get('/users/:id', async (c) => c.json({ id: c.req.param('id'), name: 'Alice' }))` 这类链式写法同时声明 API 与校验。
- 把所有路由链式赋给一个变量,如 `const route = app.get(...).post(...).get(...)`,不要拆成多条独立语句,否则路由类型会丢失。
- 通过 `export type AppType = typeof route` 从服务端导出完整 API 类型,供客户端直接引用。
- 客户端从 `hono/client` 导入 `hc`,从服务端导入 `type AppType`,用 `const client = hc<AppType>('http://localhost:3000')` 创建类型安全客户端。
- 调用 GET 路由时用 `const usersRes = await client.users.$get()`,再用 `const { users } = await usersRes.json()` 获取已推断类型的响应。
- 调用 POST 路由时用 `await client.users.$post({ json: { name: 'Bob', email: 'bob@example.com' } })`,请求体形状会由服务端 `zValidator('json', ...)` 自动推断。
- 路径参数:服务端写 `.get('/posts/:postId/comments/:commentId', async (c) => { const { postId, commentId } = c.req.param(); return c.json({ postId, commentId }) })`;客户端写 `client.posts[':postId'].comments[':commentId'].$get({ param: { postId: '1', commentId: '42' } })`。
- 查询参数:服务端用 `zValidator('query', z.object({ q: z.string(), page: z.coerce.number().optional(), limit: z.coerce.number().optional() }))`,并用 `const { q, page, limit } = c.req.valid('query')` 读取;客户端用 `client.search.$get({ query: { q: 'typescript', page: 1, limit: 20 } })`。
- JSON body:服务端用 `zValidator('json', z.object({ title: z.string(), content: z.string(), tags: z.array(z.string()).optional() }))`,并用 `const data = c.req.valid('json')` 读取;客户端用 `client.posts.$post({ json: { title: 'Hello World', content: 'My first post', tags: ['typescript', 'hono'] } })`。
- Form data:服务端用 `zValidator('form', z.object({ file: z.instanceof(File), description: z.string().optional() }))`,并用 `const { file, description } = c.req.valid('form')` 读取;客户端创建 `FormData` 并 append 后调用 `client.upload.$post({ form: formData })`。
- Headers:服务端用 `zValidator('header', z.object({ authorization: z.string() }))`;客户端用 `client.protected.$get({ header: { authorization: 'Bearer token123' } })`。
- 状态码感知响应类型:服务端显式返回 `return c.json({ data }, 200)`、`return c.json({ error: 'Not found' }, 404)`;客户端用 `InferResponseType<typeof client.user.$get, 200>` 和 `InferResponseType<typeof client.user.$get, 404>` 提取不同状态码的响应类型。
- 处理不同状态码时先判断 `if (res.status === 200)` 或 `else if (res.status === 404)`,再分别 `await res.json()`,以得到对应类型。
- 请求类型推断:从 `hono/client` 导入 `InferRequestType`,用 `type CreateUserRequest = InferRequestType<typeof client.users.$post>['json']` 提取请求体类型,用于表单校验、状态管理等。
- 多文件路由组织:每个路由文件导出 `new Hono()` 链式实例,例如 `server/routes/users.ts` 导出 `users`,`server/routes/posts.ts` 导出 `posts`,并在文件内链式定义 `.get('/')`、`.post('/')`、`.get('/:id')`。
- 服务端入口用 `const route = app.route('/users', users).route('/posts', posts)` 挂载子路由,然后 `export type AppType = typeof route`、`export default app`。
- 客户端按嵌套路径调用多文件路由,如 `client.users.$get()`、`client.users[':id'].$get()`、`client.posts.$get()`。
- 错误处理:把客户端调用放在 `try/catch` 中,先检查 `if (!res.ok)`,再 `const error = await res.json()` 并抛出错误;捕获到 `TypeError` 时按网络错误处理。
- 类型安全错误响应:服务端返回 `{ success: true, data }` 或 `{ success: false, error: 'Failed' }` 并配状态码;客户端定义 `type ApiResponse<T> = { success: true; data: T } | { success: false; error: string }`,再按 `result.success` 分支处理。
- 自定义 fetch:创建客户端时传 `{ fetch: async (input, init) => { console.log('Fetching:', input); return fetch(input, init) } }`,用于测试、日志等。
- 默认 headers:创建客户端时传 `{ headers: { 'Authorization': 'Bearer token', 'X-Custom-Header': 'value' } }`。
- 动态 headers:用 `const getClient = (token: string) => hc<AppType>('http://localhost:3000', { headers: () => ({ 'Authorization': `Bearer ${token}` }) })`,或在 `headers` 函数中读取 `getAuthToken()` 后返回 headers 对象。
- 在 `tsconfig.json` 的 `compilerOptions` 中启用 `strict: true`,这是正确类型推断的必要设置。
- 尽量显式返回状态码,如 `return c.json({ data }, 200)`、`return c.json({ error: 'Not found' }, 404)`;避免使用 `c.notFound()`,因为它的 RPC 响应类型推断不理想。
- 大型应用拆分:按版本/模块创建多个 Hono 实例,如 `const v1 = new Hono().route('/users', usersRoute).route('/posts', postsRoute)`、`const v2 = new Hono().route('/users', usersV2Route)`,并分别 `export type V1Type = typeof v1`、`export type V2Type = typeof v2`,以降低 IDE 负担。
- 统一响应形状:定义 `type ApiSuccess<T> = { ok: true; data: T }`、`type ApiError = { ok: false; error: string; code?: string }`、`type ApiResponse<T> = ApiSuccess<T> | ApiError`,并在所有路由一致使用。
- 客户端方法映射:GET 用 `client.path.$get()`,POST 用 `client.path.$post()`,PUT 用 `client.path.$put()`,DELETE 用 `client.path.$delete()`,PATCH 用 `client.path.$patch()`。
- 请求选项按类型传入:`client.path.$method({ param: { id: '1' }, query: { page: 1 }, json: { name: 'Alice' }, form: formData, header: { 'X-Custom': 'v' } })`,分别对应路径参数、查询参数、JSON body、FormData、请求头。
- 类型工具:从 `hono/client` 导入 `InferRequestType` 和 `InferResponseType`,用 `InferRequestType<typeof client.users.$post>` 提取请求类型,用 `InferResponseType<typeof client.users.$get, 200>` / `404` 提取响应类型。
- 所有请求校验统一使用 `@hono/zod-validator` 的 `zValidator` 和 `zod` schema,并用 `c.req.valid('json' | 'query' | 'form' | 'header')` 读取,这样 Hono RPC 才能把请求形状推断到客户端。
适用边界 · 注意事项
- 客户端不是 TypeScript、无法直接引用服务端导出的 AppType,或需要跨语言/第三方 API 时,不适合;应改用 OpenAPI 或代码生成。
- 服务端路由未链式定义、未导出 typeof route,或客户端与服务端类型无法共享(如独立仓库、独立部署且没有共享类型包)时,类型推断会失效。
- 需要严格解耦前后端 API 契约、独立发布版本化 SDK,或禁止客户端依赖服务端源码类型时,不适合。