mcpField
H
hono-routing

hono-routing

github.com/jezweb开发1.1k615 次安装3 周前更新
代码仓库

关于 hono-routing

面向 Hono 开发者的路由与中间件技能,涵盖路由组织、请求校验、类型安全上下文和流式响应,也说明不同运行环境下的注意事项。

主要功能

  1. 01组织路由与子应用
  2. 02配置内置及自定义中间件
  3. 03校验请求数据并推导类型
  4. 04扩展类型安全的上下文
  5. 05处理流式响应与 WebSocket

适用场景

  1. 01构建带参数约束的 API 路由
  2. 02为请求正文、查询参数或标头添加校验
  3. 03为指定路由配置认证或安全中间件
  4. 04实现文本流、SSE 或 WebSocket 端点

安装

复制下面的命令,在终端中运行。

$npx skills add https://github.com/jezweb/claude-skills --skill hono-routing

安装后在 Claude Code 中输入/hono-routing确认已加载。

包含的文件

除 SKILL.md 外,安装时一起下载的参考资料和脚本。

README.md
.claude-plugin/
plugin.json
references/
middleware-catalog.md
rpc-guide.md
top-errors.md
validation-libraries.md
rules/
hono-routing.md
scripts/
check-versions.sh
templates/
context-extension.ts
error-handling.ts
middleware-composition.ts
package.json
routing-patterns.ts
rpc-client.ts
rpc-pattern.ts
validation-valibot.ts
validation-zod.ts

详细介绍

Hono 路由与中间件

状态:可用于生产环境 ✅ 最后更新:2026-01-20 依赖项:无(与框架无关) 最新版本:[email protected]、[email protected]、[email protected]、@hono/[email protected]、@hono/[email protected]


快速开始(15 分钟)

1. 安装 Hono
npm install [email protected]

为什么选择 Hono:

  • 快速:基于 Web 标准构建,可在任何 JavaScript 运行时中运行
  • 轻量:约 10KB,无依赖项
  • 类型安全:全面支持 TypeScript,并提供类型推断
  • 灵活:可运行于 Cloudflare Workers、Deno、Bun、Node.js、Vercel
2. 创建基础应用
import { Hono } from 'hono'

const app = new Hono()

app.get('/', (c) => {
  return c.json({ message: 'Hello Hono!' })
})

export default app

重要:

  • 使用 c.json()、c.text()、c.html() 返回响应
  • 返回响应(不要像 Express 那样使用 res.send())
  • 为运行时导出 app(Cloudflare Workers、Deno、Bun、Node.js)
3. 添加请求验证
npm install [email protected] @hono/[email protected]
import { zValidator } from '@hono/zod-validator'
import { z } from 'zod'

const schema = z.object({
  name: z.string(),
  age: z.number(),
})

app.post('/user', zValidator('json', schema), (c) => {
  const data = c.req.valid('json')
  return c.json({ success: true, data })
})

验证的优势:

  • 请求数据类型安全
  • 自动返回错误响应
  • 运行时验证,而不仅是 TypeScript 类型检查

Hono 精通指南:4 个部分

第 1 部分:路由模式
路由参数
// Single parameter
app.get('/users/:id', (c) => {
  const id = c.req.param('id')
  return c.json({ userId: id })
})

// Multiple parameters
app.get('/posts/:postId/comments/:commentId', (c) => {
  const { postId, commentId } = c.req.param()
  return c.json({ postId, commentId })
})

// Optional parameters (using wildcards)
app.get('/files/*', (c) => {
  const path = c.req.param('*')
  return c.json({ filePath: path })
})

重要:

  • c.req.param('name') 返回单个参数
  • c.req.param() 将所有参数作为对象返回
  • 参数始终是字符串(如有需要,可转换为数字)
路由参数正则表达式约束

在路由中使用正则表达式模式,从路由层面限制参数匹配:

// Only matches numeric IDs
app.get('/users/:id{[0-9]+}', (c) => {
  const id = c.req.param('id') // Guaranteed to be digits
  return c.json({ userId: id })
})

// Only matches UUIDs
app.get('/posts/:id{[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}}', (c) => {
  const id = c.req.param('id') // Guaranteed to be UUID format
  return c.json({ postId: id })
})

优势:

  • 在路由层面提前验证
  • 防止无效请求到达处理器
  • 路由约束自带说明
查询参数
app.get('/search', (c) => {
  // Single query param
  const q = c.req.query('q')

  // Multiple query params
  const { page, limit } = c.req.query()

  // Query param array (e.g., ?tag=js&tag=ts)
  const tags = c.req.queries('tag')

  return c.json({ q, page, limit, tags })
})

最佳实践:

  • 对查询参数使用验证(见第 4 部分)
  • 为可选参数提供默认值
  • 将查询字符串中的数字和布尔值解析为对应类型
路由分组(子应用)
// Create sub-app
const api = new Hono()

api.get('/users', (c) => c.json({ users: [] }))
api.get('/posts', (c) => c.json({ posts: [] }))

// Mount sub-app
const app = new Hono()
app.route('/api', api)

// Result: /api/users, /api/posts

路由分组的优势:

  • 组织大型应用
  • 为特定路由共享中间件
  • 改善代码结构和可维护性

第 2 部分:中间件与验证

中间件重要规则:

  • 始终在中间件中调用 await next(),以继续执行链
  • 提前返回(不调用 next())可阻止处理器执行
  • 调用 next() 后检查 c.error,以处理错误
app.use('/admin/*', async (c, next) => {
  const token = c.req.header('Authorization')
  if (!token) return c.json({ error: 'Unauthorized' }, 401)
  await next() // Required!
})
内置中间件
import { Hono } from 'hono'
import { logger } from 'hono/logger'
import { cors } from 'hono/cors'
import { prettyJSON } from 'hono/pretty-json'
import { compress } from 'hono/compress'
import { cache } from 'hono/cache'

const app = new Hono()

// Request logging
app.use('*', logger())

// CORS
app.use('/api/*', cors({
  origin: 'https://example.com',
  allowMethods: ['GET', 'POST', 'PUT', 'DELETE'],
  allowHeaders: ['Content-Type', 'Authorization'],
}))

// Pretty JSON (dev only)
app.use('*', prettyJSON())

// Compression (gzip/deflate)
app.use('*', compress())

// Cache responses
app.use(
  '/static/*',
  cache({
    cacheName: 'my-app',
    cacheControl: 'max-age=3600',
  })
)

自定义缓存中间件模式:

为 Node.js(或其他非 Cloudflare 运行时)实现自定义缓存中间件时,必须先克隆响应,再将其存入缓存:

const cache = new Map<string, Response>()

const customCache = async (c, next) => {
  const key = c.req.url

  // Check cache
  const cached = cache.get(key)
  if (cached) {
    return cached.clone() // Clone when returning from cache
  }

  // Execute handler
  await next()

  // Store in cache (must clone!)
  cache.set(key, c.res.clone()) // ✅ Clone before storing
}

app.use('*', customCache)

必须克隆的原因: 响应正文是只能读取一次的可读流。克隆会创建一个带有全新流的新响应。


**Built-in Middleware Reference**: See `references/middleware-catalog.md`

#### Streaming Helpers (SSE, AI Responses)

```typescript
import { Hono } from 'hono'
import { stream, streamText, streamSSE } from 'hono/streaming'

const app = new Hono()

// Binary streaming
app.get('/download', (c) => {
  return stream(c, async (stream) => {
    await stream.write(new Uint8Array([0x48, 0x65, 0x6c, 0x6c, 0x6f]))
    await stream.pipe(readableStream)
  })
})

// Text streaming (AI responses)
app.get('/ai', (c) => {
  return streamText(c, async (stream) => {
    for await (const chunk of aiResponse) {
      await stream.write(chunk)
      await stream.sleep(50) // Rate limit if needed
    }
  })
})

// Server-Sent Events (real-time updates)
app.get('/sse', (c) => {
  return streamSSE(c, async (stream) => {
    let id = 0
    while (true) {
      await stream.writeSSE({
        data: JSON.stringify({ time: Date.now() }),
        event: 'update',
        id: String(id++),
      })
      await stream.sleep(1000)
    }
  })
})

适用场景:

  • stream() - 二进制文件、视频、音频
  • streamText() - AI 聊天响应、打字机效果
  • streamSSE() - 实时通知、实时信息流
WebSocket 辅助工具
import { Hono } from 'hono'
import { upgradeWebSocket } from 'hono/cloudflare-workers' // Platform-specific!

const app = new Hono()

app.get('/ws', upgradeWebSocket((c) => ({
  onMessage(event, ws) {
    console.log(`Message: ${event.data}`)
    ws.send(`Echo: ${event.data}`)
  },
  onClose: () => console.log('Closed'),
  onError: (event) => console.error('Error:', event),
  // onOpen is NOT supported on Cloudflare Workers!
})))

export default app

⚠️ Cloudflare Workers WebSocket 注意事项:

  • 从 hono/cloudflare-workers 导入(而非 hono/ws)
  • 不支持 onOpen 回调(Cloudflare 限制)
  • CORS/修改标头的中间件会与 WebSocket 路由冲突
  • 使用路由分组,将 WebSocket 路由排除在 CORS 之外:
const api = new Hono()
api.use('*', cors()) // CORS for API only
app.route('/api', api)
app.get('/ws', upgradeWebSocket(...)) // No CORS on WebSocket
安全中间件
import { Hono } from 'hono'
import { secureHeaders } from 'hono/secure-headers'
import { csrf } from 'hono/csrf'

const app = new Hono()

// Security headers (X-Frame-Options, CSP, HSTS, etc.)
app.use('*', secureHeaders({
  xFrameOptions: 'DENY',
  xXssProtection: '1; mode=block',
  contentSecurityPolicy: {
    defaultSrc: ["'self'"],
    scriptSrc: ["'self'", "'unsafe-inline'"],
  },
}))

// CSRF protection (validates Origin header)
app.use('/api/*', csrf({
  origin: ['https://example.com', 'https://admin.example.com'],
}))

安全中间件选项:

中间件用途
secureHeadersX-Frame-Options、CSP、HSTS、XSS 防护
csrf通过 Origin/Sec-Fetch-Site 验证防范 CSRF
bearerAuthBearer 令牌身份验证
basicAuthHTTP Basic 身份验证
ipRestrictionIP 允许列表/阻止列表
组合中间件

使用条件逻辑组合中间件:

import { Hono } from 'hono'
import { some, every, except } from 'hono/combine'
import { bearerAuth } from 'hono/bearer-auth'
import { ipRestriction } from 'hono/ip-restriction'

const app = new Hono()

// some: ANY middleware must pass (OR logic)
app.use('/admin/*', some(
  bearerAuth({ token: 'admin-token' }),
  ipRestriction({ allowList: ['10.0.0.0/8'] }),
))

// every: ALL middleware must pass (AND logic)
app.use('/secure/*', every(
  bearerAuth({ token: 'secret' }),
  ipRestriction({ allowList: ['192.168.1.0/24'] }),
))

// except: Skip middleware for certain paths
app.use('*', except(
  ['/health', '/metrics'],
  logger(),
))

第 3 部分:类型安全的上下文扩展
使用 c.set() 和 c.get()
import { Hono } from 'hono'

type Bindings = {
  DATABASE_URL: string
}

type Variables = {
  user: {
    id: number
    name: string
  }
  requestId: string
}

const app = new Hono<{ Bindings: Bindings; Variables: Variables }>()

// Middleware sets variables
app.use('*', async (c, next) => {
  c.set('requestId', crypto.randomUUID())
  await next()
})

app.use('/api/*', async (c, next) => {
  c.set('user', { id: 1, name: 'Alice' })
  await next()
})

// Route accesses variables
app.get('/api/profile', (c) => {
  const user = c.get('user') // Type-safe!
  const requestId = c.get('requestId') // Type-safe!

  return c.json({ user, requestId })
})

重要:

  • 定义 Variables 类型,确保 c.get() 类型安全
  • 定义 Bindings 类型,用于环境变量(Cloudflare Workers)
  • 在中间件中使用 c.set(),在处理器中使用 c.get()
自定义上下文扩展
import { Hono } from 'hono'
import type { Context } from 'hono'

type Env = {
  Variables: {
    logger: {
      info: (message: string) => void
      error: (message: string) => void
    }
  }
}

const app = new Hono<Env>()

// Create logger middleware
app.use('*', async (c, next) => {
  const logger = {
    info: (msg: string) => console.log(`[INFO] ${msg}`),
    error: (msg: string) => console.error(`[ERROR] ${msg}`),
  }

  c.set('logger', logger)
  await next()
})

app.get('/', (c) => {
  const logger = c.get('logger')
  logger.info('Hello from route')

  return c.json({ message: 'Hello' })
})

高级模式:参见 templates/context-extension.ts


第 4 部分:请求验证
使用 Zod 进行验证
npm install [email protected] @hono/[email protected]
import { zValidator } from '@hono/zod-validator'
import { z } from 'zod'

// Define schema
const userSchema = z.object({
  name: z.string().min(1).max(100),
  email: z.string().email(),
  age: z.number().int().min(18).optional(),
})

// Validate JSON body
app.post('/users', zValidator('json', userSchema), (c) => {
  const data = c.req.valid('json') // Type-safe!
  return c.json({ success: true, data })
})

// Validate query params
const searchSchema = z.object({
  q: z.string(),
  page: z.string().transform((val) => parseInt(val, 10)),
  limit: z.string().transform((val) => parseInt(val, 10)).optional(),
})

app.get('/search', zValidator('query', searchSchema), (c) => {
  const { q, page, limit } = c.req.valid('query')
  return c.json({ q, page, limit })
})

// Validate route params
const idSchema = z.object({
  id: z.string().uuid(),
})

app.get('/users/:id', zValidator('param', idSchema), (c) => {
  const { id } = c.req.valid('param')
  return c.json({ userId: id })
})

// Validate headers
const headerSchema = z.object({
  'authorization': z.string().startsWith('Bearer '),
  'content-type': z.string(),
})

app.post('/auth', zValidator('header', headerSchema), (c) => {
  const headers = c.req.valid('header')
  return c.json({ authenticated: true })
})

重要:

  • 验证后始终使用 c.req.valid()(类型安全)
  • 验证目标:json、query、param、header、form、cookie
  • 使用 z.transform() 将字符串转换为数字或日期
  • 验证错误会自动返回 400

⚠️ 重要:验证必须针对具体处理程序

要正确推断经过验证的类型,必须在处理程序中添加验证中间件,而不是通过 app.use() 添加:

// ❌ WRONG - Type inference breaks
app.use('/users', zValidator('json', userSchema))

app.post('/users', (c) => {
  const data = c.req.valid('json') // TS Error: Type 'never'
  return c.json({ data })
})

// ✅ CORRECT - Validation in handler
app.post('/users', zValidator('json', userSchema), (c) => {
  const data = c.req.valid('json') // Type-safe!
  return c.json({ data })
})

原因: Hono 的 Input 类型映射使用泛型合并验证结果。通过 app.use() 应用验证器时,类型系统无法跟踪哪些路由使用了哪些验证架构,导致 Input 泛型折叠为 never。

自定义验证钩子
import { zValidator } from '@hono/zod-validator'
import { HTTPException } from 'hono/http-exception'

const schema = z.object({
  name: z.string(),
  age: z.number(),
})

// Custom error handler
app.post(
  '/users',
  zValidator('json', schema, (result, c) => {
    if (!result.success) {
      // Custom error response
      return c.json(
        {
          error: 'Validation failed',
          issues: result.error.issues,
        },
        400
      )
    }
  }),
  (c) => {
    const data = c.req.valid('json')
    return c.json({ success: true, data })
  }
)

// Throw HTTPException
app.post(
  '/users',
  zValidator('json', schema, (result, c) => {
    if (!result.success) {
      throw new HTTPException(400, { cause: result.error })
    }
  }),
  (c) => {
    const data = c.req.valid('json')
    return c.json({ success: true, data })
  }
)

关于 Zod 可选枚举的说明: 在 @hono/[email protected] 之前,可选枚举会被错误地解析为字符串,而不是枚举类型。此问题已在 v0.7.6 中修复。请确保使用最新版本:

npm install @hono/[email protected]
使用 Valibot 进行验证
npm install [email protected] @hono/[email protected]
import { vValidator } from '@hono/valibot-validator'
import * as v from 'valibot'

const schema = v.object({
  name: v.string(),
  age: v.number(),
})

app.post('/users', vValidator('json', schema), (c) => {
  const data = c.req.valid('json')
  return c.json({ success: true, data })
})

Zod 与 Valibot 的对比:请参阅 references/validation-libraries.md

使用 Typia 进行验证
npm install typia @hono/[email protected]
import { typiaValidator } from '@hono/typia-validator'
import typia from 'typia'

interface User {
  name: string
  age: number
}

const validate = typia.createValidate<User>()

app.post('/users', typiaValidator('json', validate), (c) => {
  const data = c.req.valid('json')
  return c.json({ success: true, data })
})

选择 Typia 的原因:

  • 验证速度最快(编译时)
  • 无需在运行时定义架构
  • AOT(提前编译)
使用 ArkType 进行验证
npm install arktype @hono/[email protected]
import { arktypeValidator } from '@hono/arktype-validator'
import { type } from 'arktype'

const schema = type({
  name: 'string',
  age: 'number',
})

app.post('/users', arktypeValidator('json', schema), (c) => {
  const data = c.req.valid('json')
  return c.json({ success: true, data })
})

对比:详细对比请参阅 references/validation-libraries.md


第 5 部分:类型化路由(RPC)
为什么使用 RPC?

Hono 的 RPC 功能支持类型安全的客户端/服务端通信,无需手动定义 API 类型。客户端可直接从服务端路由推断类型。

服务端设置
// app.ts
import { Hono } from 'hono'
import { zValidator } from '@hono/zod-validator'
import { z } from 'zod'

const app = new Hono()

const schema = z.object({
  name: z.string(),
  age: z.number(),
})

// Define route and export type
const route = app.post(
  '/users',
  zValidator('json', schema),
  (c) => {
    const data = c.req.valid('json')
    return c.json({ success: true, data }, 201)
  }
)

// Export app type for RPC client
export type AppType = typeof route

// OR export entire app
// export type AppType = typeof app

export default app

重要:

  • 必须使用 const route = app.get(...) 进行 RPC 类型推断
  • 导出 typeof route 或 typeof app
  • 不要使用匿名路由定义
客户端设置
// client.ts
import { hc } from 'hono/client'
import type { AppType } from './app'

const client = hc<AppType>('http://localhost:8787')

// Type-safe API call
const res = await client.users.$post({
  json: {
    name: 'Alice',
    age: 30,
  },
})

// Response is typed!
const data = await res.json() // { success: boolean, data: { name: string, age: number } }

使用 RPC 的原因:

  • ✅ 完整的类型推断(请求 + 响应)
  • ✅ 无需手动定义类型
  • ✅ 编译时错误检查
  • ✅ IDE 中的自动补全

⚠️ RPC 类型推断的限制: RPC 客户端仅会为 json 和 text 响应推断类型。如果某个端点返回多种响应类型(例如 JSON 和二进制数据),所有响应都不会进行类型推断:

// ❌ Type inference fails - mixes JSON and binary
app.post('/upload', async (c) => {
  const body = await c.req.body() // Binary response
  if (error) {
    return c.json({ error: 'Bad request' }, 400) // JSON response
  }
  return c.json({ success: true })
})

// ✅ Separate endpoints by response type
app.post('/upload', async (c) => {
  return c.json({ success: true }) // Only JSON - types work
})

app.get('/download/:id', async (c) => {
  return c.body(binaryData) // Only binary - separate endpoint
})
使用多个路由的 RPC
// Server
const app = new Hono()

const getUsers = app.get('/users', (c) => {
  return c.json({ users: [] })
})

const createUser = app.post(
  '/users',
  zValidator('json', userSchema),
  (c) => {
    const data = c.req.valid('json')
    return c.json({ success: true, data }, 201)
  }
)

const getUser = app.get('/users/:id', (c) => {
  const id = c.req.param('id')
  return c.json({ id, name: 'Alice' })
})

// Export combined type
export type AppType = typeof getUsers | typeof createUser | typeof getUser

// Client
const client = hc<AppType>('http://localhost:8787')

// GET /users
const usersRes = await client.users.$get()

// POST /users
const createRes = await client.users.$post({
  json: { name: 'Alice', age: 30 },
})

// GET /users/:id
const userRes = await client.users[':id'].$get({
  param: { id: '123' },
})
RPC 性能优化

问题:路由众多的大型应用会导致类型推断缓慢

解决方案:导出特定路由组,而不是整个应用

// ❌ Slow: Export entire app
export type AppType = typeof app

// ✅ Fast: Export specific routes
const userRoutes = app.get('/users', ...).post('/users', ...)
export type UserRoutes = typeof userRoutes

const postRoutes = app.get('/posts', ...).post('/posts', ...)
export type PostRoutes = typeof postRoutes

// Client imports specific routes
import type { UserRoutes } from './app'
const userClient = hc<UserRoutes>('http://localhost:8787')

深入了解:请参阅 references/rpc-guide.md


第 6 部分:错误处理
HTTPException
import { Hono } from 'hono'
import { HTTPException } from 'hono/http-exception'

const app = new Hono()

app.get('/users/:id', (c) => {
  const id = c.req.param('id')

  // Throw HTTPException for client errors
  if (!id) {
    throw new HTTPException(400, { message: 'ID is required' })
  }

  // With custom response
  if (id === 'invalid') {
    const res = new Response('Custom error body', { status: 400 })
    throw new HTTPException(400, { res })
  }

  return c.json({ id })
})

重要:

  • 对于预期的错误(400、401、403、404),请使用 HTTPException
  • 对于意外的错误(500),不要使用 HTTPException;请改用 onError
  • HTTPException 会立即停止执行
全局错误处理程序(onError)
import { Hono } from 'hono'
import { HTTPException } from 'hono/http-exception'

const app = new Hono()

// Custom error handler
app.onError((err, c) => {
  // Handle HTTPException
  if (err instanceof HTTPException) {
    return err.getResponse()
  }

  // Handle unexpected errors
  console.error('Unexpected error:', err)

  return c.json(
    {
      error: 'Internal Server Error',
      message: err.message,
    },
    500
  )
})

app.get('/error', (c) => {
  throw new Error('Something went wrong!')
})

使用 onError 的原因:

  • 集中处理错误
  • 提供一致的错误响应
  • 错误日志记录与跟踪
中间件错误检查
app.use('*', async (c, next) => {
  await next()

  // Check for errors after handler
  if (c.error) {
    console.error('Error in route:', c.error)
    // Send to error tracking service
  }
})
未找到处理程序
app.notFound((c) => {
  return c.json({ error: 'Not Found' }, 404)
})

关键规则

始终执行

✅ 在中间件中调用 await next()——这是执行中间件链所必需的 ✅ 处理程序必须返回 Response——使用 c.json()、c.text()、c.html() ✅ 验证后使用 c.req.valid()——类型安全地获取验证后的数据 ✅ 为 RPC 导出路由类型——export type AppType = typeof route ✅ 对客户端错误抛出 HTTPException——400、401、403、404 错误 ✅ 使用 onError 进行全局错误处理——集中处理错误响应 ✅ 为 c.set/c.get 定义 Variables 类型——确保上下文变量的类型安全 ✅ 使用 const route = app.get(...)——这是进行 RPC 类型推断所必需的

切勿这样做

❌ 在中间件中忘记 await next() - 会中断中间件链 ❌ 像使用 Express 一样使用 res.send() - 与 Hono 不兼容 ❌ 未经验证就访问请求数据 - 使用验证器确保类型安全 ❌ 为大型 RPC 导出整个应用 - 会拖慢类型推断,请导出特定路由 ❌ 使用普通的 throw new Error() - 请改用 HTTPException ❌ 跳过 onError 处理器 - 会导致错误响应不一致 ❌ 未定义 Variables 类型就使用 c.set/c.get - 会失去类型安全


已知问题预防

此技能可预防 10 个已记录的问题:

问题 #1:RPC 类型推断缓慢

错误:路由过多时 IDE 会变慢(CI 构建耗时 8 分钟,IntelliSense 无法使用) 来源:hono/docs/guides/rpc | GitHub Issue #3869 原因:路由过多时,typeof app 会导致复杂的类型实例化。Zod 的 omit、extend、pick 等方法会加剧这一问题。 预防措施:导出特定路由组,而非整个应用

// ❌ Slow
export type AppType = typeof app

// ✅ Fast
const userRoutes = app.get(...).post(...)
export type UserRoutes = typeof userRoutes

大型应用的高级变通方案(100+ 个路由):

  1. 拆分为 monorepo 库:
// routers-auth/index.ts
export const authRouter = new Hono()
  .get('/login', ...)
  .post('/login', ...)

// routers-orders/index.ts
export const orderRouter = new Hono()
  .get('/orders', ...)
  .post('/orders', ...)

// routers-main/index.ts
const app = new Hono()
  .route('/auth', authRouter)
  .route('/orders', orderRouter)

export type AppType = typeof app
  1. 使用独立的构建配置:

    • 生产环境:使用完整的 tsc 并生成 .d.ts(供 RPC 客户端使用)
    • 开发环境:跳过主路由器上的 tsc,仅对子路由器进行类型检查(加快实时重新加载)
  2. 避免使用会影响性能的 Zod 方法:

    • z.omit()、z.extend()、z.pick() - 这些方法会使语言服务器的工作负载增加 10 倍
    • 尽可能使用接口,而非交叉类型
问题 #2:RPC 中间件响应没有类型

错误:RPC 客户端无法推断中间件响应(包括 notFound() 和 onError()) 来源:honojs/hono#2719 | GitHub Issue #4600 原因:RPC 模式默认不会推断中间件响应。notFound() 或 onError() 处理器返回的响应不会纳入类型映射。 预防措施:导出包含中间件的特定路由类型

const route = app.get(
  '/data',
  myMiddleware,
  (c) => c.json({ data: 'value' })
)
export type AppType = typeof route

具体问题:notFound/onError 没有类型:

// Server
const app = new Hono()
  .notFound((c) => c.json({ error: 'Not Found' }, 404))
  .get('/users/:id', async (c) => {
    const user = await getUser(c.req.param('id'))
    if (!user) {
      return c.notFound() // Type not exported to RPC client
    }
    return c.json({ user })
  })

// Client
const client = hc<typeof app>('http://localhost:8787')
const res = await client.users[':id'].$get({ param: { id: '123' } })

if (res.status === 404) {
  const error = await res.json() // Type is 'any', not { error: string }
}

部分变通方案(v4.11.0+): 使用模块增强自定义 NotFoundResponse 类型:

import { Hono, TypedResponse } from 'hono'

declare module 'hono' {
  interface NotFoundResponse
    extends Response,
      TypedResponse<{ error: string }, 404, 'json'> {}
}
问题 #3:验证钩子用法混淆

错误:不同的验证器库使用不同的钩子模式 来源:Context7 调研 原因:每个验证器(@hono/zod-validator、@hono/valibot-validator 等)的 API 都略有不同 预防措施:此技能为所有验证器提供统一的模式

问题 #4:误用 HTTPException

错误:抛出普通 Error,而非 HTTPException 来源:官方文档 原因:熟悉 Express 的开发者会使用 throw new Error() 预防措施:客户端错误(400-499)始终使用 HTTPException

// ❌ Wrong
throw new Error('Unauthorized')

// ✅ Correct
throw new HTTPException(401, { message: 'Unauthorized' })
问题 #5:上下文类型安全丢失

错误:c.set() 和 c.get() 无法进行类型推断 来源:官方文档 原因:未在 Hono 泛型中定义 Variables 类型 预防措施:始终定义 Variables 类型

type Variables = {
  user: { id: number; name: string }
}

const app = new Hono<{ Variables: Variables }>()
问题 #6:中间件之后缺少错误检查

错误:处理器中的错误未被捕获 来源:官方文档 原因:在 await next() 之后未检查 c.error 预防措施:在中间件中检查 c.error

app.use('*', async (c, next) => {
  await next()
  if (c.error) {
    console.error('Error:', c.error)
  }
})
问题 #7:未经验证直接访问请求

错误:未经验证就访问 c.req.param() 或 c.req.query() 来源:最佳实践 原因:开发者为了速度而跳过验证 预防措施:始终使用验证器和 c.req.valid()

// ❌ Wrong
const id = c.req.param('id') // string, no validation

// ✅ Correct
app.get('/users/:id', zValidator('param', idSchema), (c) => {
  const { id } = c.req.valid('param') // validated UUID
})
问题 #8:中间件顺序错误

错误:中间件执行顺序有误 来源:官方文档 原因:误解了中间件链的执行方式 预防措施:记住,中间件从上到下运行,await next() 会执行处理器,随后再从下到上返回

app.use('*', async (c, next) => {
  console.log('1: Before handler')
  await next()
  console.log('4: After handler')
})

app.use('*', async (c, next) => {
  console.log('2: Before handler')
  await next()
  console.log('3: After handler')
})

app.get('/', (c) => {
  console.log('Handler')
  return c.json({})
})

// Output: 1, 2, Handler, 3, 4
Issue #9:JWT verify() 需要算法参数(v4.11.4+)

错误:TypeError: Cannot read properties of undefined 来源:GitHub Issue #4625 | Security Advisory GHSA-f67f-6cw9-8mq4 发生原因:v4.11.4 中的安全修复要求显式指定算法,以防止 JWT 标头被篡改 预防措施:始终指定算法参数

import { verify } from 'hono/jwt'

// ❌ Wrong (pre-v4.11.4 syntax)
const payload = await verify(token, secret)

// ✅ Correct (v4.11.4+)
const payload = await verify(token, secret, 'HS256') // Algorithm required

注意:由于安全问题严重,此项破坏性变更随补丁版本发布。升级到 v4.11.4+ 时,请更新所有 JWT 验证代码。

Issue #10:请求正文被中间件读取

错误:TypeError: Body is unusable 来源:GitHub Issue #4259 发生原因:使用 c.req.raw.clone() 会绕过 Hono 的缓存并读取正文流 预防措施:始终使用 c.req.text() 或 c.req.json(),而不要直接访问原始请求

// ❌ Wrong - Breaks downstream validators
app.use('*', async (c, next) => {
  const body = await c.req.raw.clone().text() // Consumes body!
  console.log('Request body:', body)
  await next()
})

app.post('/', zValidator('json', schema), async (c) => {
  const data = c.req.valid('json') // Error: Body is unusable
  return c.json({ data })
})

// ✅ Correct - Uses cached content
app.use('*', async (c, next) => {
  const body = await c.req.text() // Cache-friendly
  console.log('Request body:', body)
  await next()
})

app.post('/', zValidator('json', schema), async (c) => {
  const data = c.req.valid('json') // Works!
  return c.json({ data })
})

原因:Web API 中的请求正文只能读取一次(它们是流)。Hono 的验证器内部会使用 await c.req.json(),并缓存内容。如果使用 c.req.raw.clone().json(),就会绕过缓存并读取正文,导致后续读取失败。


配置文件参考

package.json(完整示例)
{
  "name": "hono-app",
  "version": "1.0.0",
  "type": "module",
  "scripts": {
    "dev": "tsx watch src/index.ts",
    "build": "tsc",
    "start": "node dist/index.js"
  },
  "dependencies": {
    "hono": "^4.11.4"
  },
  "devDependencies": {
    "typescript": "^5.9.0",
    "tsx": "^4.19.0",
    "@types/node": "^22.10.0"
  }
}
使用验证的 package.json(Zod)
{
  "dependencies": {
    "hono": "^4.11.4",
    "zod": "^4.3.5",
    "@hono/zod-validator": "^0.7.6"
  }
}
使用验证的 package.json(Valibot)
{
  "dependencies": {
    "hono": "^4.11.4",
    "valibot": "^1.2.0",
    "@hono/valibot-validator": "^0.6.1"
  }
}
使用所有验证器的 package.json
{
  "dependencies": {
    "hono": "^4.11.4",
    "zod": "^4.3.5",
    "valibot": "^1.2.0",
    "@hono/zod-validator": "^0.7.6",
    "@hono/valibot-validator": "^0.6.1",
    "@hono/typia-validator": "^0.1.2",
    "@hono/arktype-validator": "^2.0.1"
  }
}
tsconfig.json
{
  "compilerOptions": {
    "target": "ES2022",
    "module": "ES2022",
    "lib": ["ES2022"],
    "moduleResolution": "bundler",
    "resolveJsonModule": true,
    "allowJs": true,
    "checkJs": false,
    "strict": true,
    "esModuleInterop": true,
    "skipLibCheck": true,
    "forceConsistentCasingInFileNames": true,
    "isolatedModules": true,
    "outDir": "./dist"
  },
  "include": ["src/**/*"],
  "exclude": ["node_modules"]
}

文件模板

所有模板均可在 templates/ 目录中找到:

  • routing-patterns.ts - 路由参数、查询参数、通配符、分组
  • middleware-composition.ts - 中间件链、内置中间件
  • validation-zod.ts - 使用自定义钩子的 Zod 验证
  • validation-valibot.ts - Valibot 验证
  • rpc-pattern.ts - 类型安全的 RPC 客户端/服务端
  • error-handling.ts - HTTPException、onError、自定义错误
  • context-extension.ts - c.set/c.get、自定义上下文类型
  • package.json - 所有依赖项

将这些文件复制到项目中,并根据需要进行自定义。


参考文档

如需深入了解,请参阅:

  • middleware-catalog.md - 完整的 Hono 内置中间件参考
  • validation-libraries.md - Zod、Valibot、Typia 和 ArkType 对比
  • rpc-guide.md - RPC 模式深入指南、性能优化
  • top-errors.md - 常见 Hono 错误及解决方案

官方文档


依赖项(最近验证于 2026-01-20)

{
  "dependencies": {
    "hono": "^4.11.4"
  },
  "optionalDependencies": {
    "zod": "^4.3.5",
    "valibot": "^1.2.0",
    "@hono/zod-validator": "^0.7.6",
    "@hono/valibot-validator": "^0.6.1",
    "@hono/typia-validator": "^0.1.2",
    "@hono/arktype-validator": "^2.0.1"
  },
  "devDependencies": {
    "typescript": "^5.9.0"
  }
}

生产环境示例

此技能已在多个运行时环境中验证:

  • Cloudflare Workers:路由、中间件、RPC 模式
  • Deno:已测试所有验证库
  • Bun:已完成性能基准测试
  • Node.js:完整测试套件通过

此技能中的所有模式均已在生产环境中验证。


有疑问或问题?

  1. 首先查看 references/top-errors.md
  2. 确认设置流程中的所有步骤都已完成
  3. 确保在中间件中调用了 await next()
  4. 确保 RPC 路由使用 const route = app.get(...) 模式
  5. 查看官方文档:https://hono.dev

评论

支持 Markdown