Chapter 05

验证与类型安全

Zod 验证器、端到端 RPC 类型推断与 OpenAPI 文档自动生成——构建零运行时错误的 API

名词解释

Zod Validator

Zod 是 TypeScript 优先的 Schema 验证库。@hono/zod-validator 将 Zod 与 Hono 无缝集成:

# 安装依赖
bun add zod @hono/zod-validator

JSON Body 验证

import { Hono } from 'hono'
import { zValidator } from '@hono/zod-validator'
import { z } from 'zod'

const app = new Hono()

// 定义 Schema
const createUserSchema = z.object({
  name: z.string().min(2, '名字至少2个字符').max(50),
  email: z.string().email('邮箱格式不正确'),
  age: z.number().int().min(0).max(150).optional(),
  role: z.enum(['user', 'admin']).default('user'),
})

// 类型自动推断(无需手动定义接口)
type CreateUser = z.infer<typeof createUserSchema>
// 等同于:{ name: string; email: string; age?: number; role: "user" | "admin" }

app.post(
  '/users',
  zValidator('json', createUserSchema),  // 验证 JSON body
  (c) => {
    const data = c.req.valid('json')  // 完全类型安全!
    // data.name → string,data.role → "user" | "admin"
    return c.json({ created: data }, 201)
  }
)

路径参数验证

const userIdSchema = z.object({
  id: z.string().uuid('ID 必须是有效的 UUID'),
})

app.get(
  '/users/:id',
  zValidator('param', userIdSchema),
  (c) => {
    const { id } = c.req.valid('param')  // id: string(UUID 格式保证)
    return c.json({ id })
  }
)

查询参数验证

const listQuerySchema = z.object({
  page: z.coerce.number().int().min(1).default(1),
  limit: z.coerce.number().int().min(1).max(100).default(20),
  search: z.string().optional(),
  sort: z.enum(['asc', 'desc']).default('desc'),
})

app.get(
  '/users',
  zValidator('query', listQuerySchema),
  (c) => {
    const { page, limit, search, sort } = c.req.valid('query')
    // page → number(z.coerce 自动将字符串转换为数字)
    return c.json({ page, limit, search, sort })
  }
)

自定义验证错误响应

zValidator('json', createUserSchema, (result, c) => {
  if (!result.success) {
    // 自定义错误响应格式
    return c.json({
      error: 'Validation failed',
      details: result.error.issues.map((issue) => ({
        field: issue.path.join('.'),
        message: issue.message,
      })),
    }, 422)
  }
})

RPC 模式:端到端类型安全

Hono RPC 是最强大的特性之一——服务端路由定义即是客户端的类型契约:

服务端定义(带类型的路由)

// server/routes/users.ts
import { Hono } from 'hono'
import { zValidator } from '@hono/zod-validator'
import { z } from 'zod'

const users = new Hono()
  .get('/', (c) => c.json({ users: [{ id: '1', name: 'Alice' }] }))
  .post(
    '/',
    zValidator('json', z.object({
      name: z.string(),
      email: z.string().email(),
    })),
    async (c) => {
      const body = c.req.valid('json')
      return c.json({ id: 'new-id', ...body }, 201)
    }
  )
  .get('/:id', (c) => c.json({ id: c.req.param('id') }))

export default users
export type UsersRoute = typeof users
// server/index.ts
import { Hono } from 'hono'
import users from './routes/users'

const app = new Hono().route('/users', users)

export default app
export type AppType = typeof app  // 导出整个 app 类型

客户端使用(完整类型推断)

// client/api.ts
import { hc } from 'hono/client'
import { type AppType } from '../server'

// 创建类型安全的 HTTP 客户端
const client = hc<AppType>('http://localhost:3000')

// 使用——完全类型安全,有 IDE 自动补全!
const getUsers = async () => {
  const res = await client.users.$get()
  const data = await res.json()
  // data.users → { id: string; name: string }[]  ← 自动推断!
  return data.users
}

const createUser = async () => {
  const res = await client.users.$post({
    json: {
      name: 'Bob',
      email: 'bob@example.com',
      // age: 'wrong'  ← TypeScript 报错!类型检查在编译时生效
    },
  })
  return await res.json()
}

const getUser = async (id: string) => {
  const res = await client.users[':id'].$get({ param: { id } })
  return await res.json()
}

RPC 的核心价值:服务端修改路由(改变路径、请求体类型、响应类型)时,客户端代码会立即出现 TypeScript 编译错误,无需等到运行时才发现问题。这彻底消灭了前后端接口不一致的问题,是现代全栈开发的最佳实践。

OpenAPI 文档生成

# 安装
bun add @hono/zod-openapi
import { OpenAPIHono, createRoute } from '@hono/zod-openapi'
import { z } from 'zod'

const app = new OpenAPIHono()

// 使用 createRoute 定义带 OpenAPI 元数据的路由
const getUserRoute = createRoute({
  method: 'get',
  path: '/users/{id}',
  summary: 'Get a user by ID',
  tags: ['Users'],
  request: {
    params: z.object({ id: z.string().openapi({ example: '123' }) }),
  },
  responses: {
    200: {
      description: 'User found',
      content: {
        'application/json': {
          schema: z.object({
            id: z.string(),
            name: z.string(),
            email: z.string().email(),
          }),
        },
      },
    },
    404: { description: 'User not found' },
  },
})

app.openapi(getUserRoute, (c) => {
  const { id } = c.req.valid('param')
  return c.json({ id, name: 'Alice', email: 'alice@example.com' })
})

// 自动生成 OpenAPI JSON
app.doc('/openapi.json', {
  openapi: '3.0.0',
  info: { title: 'My API', version: '1.0.0' },
})

// Swagger UI(需要额外安装 @hono/swagger-ui)
import { swaggerUI } from '@hono/swagger-ui'
app.get('/docs', swaggerUI({ url: '/openapi.json' }))

Zod Validator vs OpenAPI HonozValidator 适合快速开发,只需要验证功能;OpenAPIHono + createRoute 适合需要同时提供 Swagger 文档的项目,代码量更多但 API 文档自动生成。两者的验证逻辑都是 Zod,可以根据项目需求选择。