Chapter 10

全栈应用与最佳实践

Hono RPC + React Query、结构化日志、测试策略与项目架构——从 Demo 到生产级应用的完整路径

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、拥抱边缘计算的未来。