Hono RPC + React Query:类型安全全栈
将 Hono RPC(第5章)与 React Query 结合,实现从数据库到 React 组件的端到端类型安全:
项目结构(Monorepo 风格)
fullstack-app/
├── server/ # Hono 后端
│ ├── src/
│ │ ├── index.ts
│ │ └── routes/
│ └── package.json
├── client/ # React + Vite 前端
│ ├── src/
│ │ ├── lib/api.ts # Hono 客户端
│ │ └── hooks/
│ └── package.json
└── package.json # 根 workspace
服务端(Hono 类型导出)
// server/src/routes/todos.ts
import { Hono } from 'hono'
import { zValidator } from '@hono/zod-validator'
import { z } from 'zod'
type Todo = { id: string; title: string; done: boolean }
const store: Map<string, Todo> = new Map()
const todos = new Hono()
.get('/', (c) => c.json({ todos: Array.from(store.values()) }))
.post(
'/',
zValidator('json', z.object({ title: z.string().min(1) })),
(c) => {
const { title } = c.req.valid('json')
const todo: Todo = { id: crypto.randomUUID(), title, done: false }
store.set(todo.id, todo)
return c.json(todo, 201)
}
)
.patch('/:id', zValidator('json', z.object({ done: z.boolean() })), (c) => {
const id = c.req.param('id')
const todo = store.get(id)
if (!todo) return c.json({ error: 'Not found' }, 404)
todo.done = c.req.valid('json').done
return c.json(todo)
})
export default todos
export type TodosRoute = typeof todos
客户端(React Query 集成)
// client/src/lib/api.ts
import { hc } from 'hono/client'
import { type AppType } from '../../server/src'
export const api = hc<AppType>('http://localhost:3000')
// client/src/hooks/useTodos.ts
import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query'
import { api } from '../lib/api'
export function useTodos() {
return useQuery({
queryKey: ['todos'],
queryFn: async () => {
const res = await api.todos.$get()
return res.json() // 自动推断类型:{ todos: Todo[] }
},
})
}
export function useCreateTodo() {
const qc = useQueryClient()
return useMutation({
mutationFn: async (title: string) => {
const res = await api.todos.$post({ json: { title } })
return res.json()
},
onSuccess: () => qc.invalidateQueries({ queryKey: ['todos'] }),
})
}
统一错误处理
// src/index.ts — 全局错误处理
import { Hono } from 'hono'
// 自定义错误类型
class AppError extends Error {
constructor(
message: string,
public readonly statusCode: number = 500,
public readonly code: string = 'INTERNAL_ERROR'
) {
super(message)
this.name = 'AppError'
}
}
const app = new Hono()
// 全局错误处理器
app.onError((err, c) => {
console.error({
message: err.message,
stack: err.stack,
path: c.req.path,
method: c.req.method,
})
if (err instanceof AppError) {
return c.json({
error: { code: err.code, message: err.message }
}, err.statusCode as any)
}
// 生产环境不暴露内部错误详情
return c.json({
error: { code: 'INTERNAL_ERROR', message: 'Something went wrong' }
}, 500)
})
// 404 处理
app.notFound((c) => {
return c.json({
error: { code: 'NOT_FOUND', message: `Route ${c.req.path} not found` }
}, 404)
})
// 在路由中使用
app.get('/users/:id', async (c) => {
const user = await findUser(c.req.param('id'))
if (!user) throw new AppError('User not found', 404, 'USER_NOT_FOUND')
return c.json(user)
})
结构化日志(hono-pino)
// 在 Node.js/Bun 环境中使用 pino 结构化日志
bun add pino pino-pretty
// src/lib/logger.ts
import pino from 'pino'
export const logger = pino({
level: process.env.LOG_LEVEL ?? 'info',
transport: process.env.NODE_ENV !== 'production'
? { target: 'pino-pretty', options: { colorize: true } }
: undefined,
// 生产环境输出 JSON(便于日志聚合系统处理)
})
// 请求日志中间件
export const requestLogger = async (c: any, next: () => Promise<void>) => {
const start = Date.now()
const reqId = crypto.randomUUID().slice(0, 8)
logger.info({ reqId, method: c.req.method, path: c.req.path }, 'Request received')
await next()
logger.info({
reqId,
method: c.req.method,
path: c.req.path,
status: c.res.status,
duration: `${Date.now() - start}ms`,
}, 'Request completed')
}
项目最终结构
src/
├── index.ts # 入口:app 创建、中间件注册、路由挂载
├── routes/
│ ├── auth.ts # 登录/注册/刷新 Token
│ ├── users.ts # 用户 CRUD
│ └── posts.ts # 文章 CRUD
├── middleware/
│ ├── auth.ts # JWT 验证中间件
│ ├── rate-limit.ts # 限流中间件
│ └── timing.ts # 响应时间记录
├── db/
│ ├── index.ts # DB 实例创建
│ └── schema.ts # Drizzle Schema 定义
├── lib/
│ ├── errors.ts # AppError 类定义
│ └── logger.ts # 日志工具
└── types.ts # 全局类型:Env、Variables
测试策略
// src/__tests__/posts.test.ts
import { describe, it, expect, beforeEach } from 'bun:test'
import app from '..'
describe('Posts API', () => {
let authToken: string
beforeEach(async () => {
// 创建测试用户并获取 token
const res = await app.request('/api/auth/login', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ email: 'test@example.com', password: 'Test1234!' }),
})
const data = await res.json()
authToken = data.accessToken
})
it('creates a post with tags', async () => {
const res = await app.request('/api/posts', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
Authorization: `Bearer ${authToken}`,
},
body: JSON.stringify({
title: 'Test Post',
content: 'Content here',
tagNames: ['hono', 'typescript'],
}),
})
expect(res.status).toBe(201)
const post = await res.json()
expect(post.title).toBe('Test Post')
expect(post.tags).toContain('hono')
})
})
何时选 Hono:适合 vs 不适合
| 场景 | 建议 | 理由 |
|---|---|---|
| Cloudflare Workers API | 强烈推荐 | 为此场景而生,原生支持所有 Bindings |
| Bun / Deno 微服务 | 推荐 | 极速路由 + 原生 TS,开发体验一流 |
| API Gateway / BFF | 推荐 | 轻量、多运行时、RPC 类型安全 |
| Vercel Edge Functions | 推荐 | 内置适配器,与 Next.js 集成良好 |
| AWS Lambda | 可用 | 内置适配器,但 Fastify 生态更成熟 |
| 传统 Node.js 大型项目 | 考虑需求 | Fastify/NestJS 生态更丰富,文档更多 |
| 需要 GraphQL | 选其他 | 用 Apollo Server / Pothos,有专门框架 |
| 实时游戏服务器 | 不推荐 | 复杂状态管理超出 Hono 设计范围 |
课程总结:Hono 以"炎"为名,代表着 Web 框架领域的一股新火——零依赖、多运行时、类型安全、极速路由。从 Cloudflare Workers 的边缘部署到 Bun 的本地开发,从 Zod 验证到 RPC 端到端类型安全,Hono 证明了轻量与功能完整可以并存。选择 Hono,就是选择拥抱 Web Standards、拥抱边缘计算的未来。