Skip to main content

Best Practices

还没翻译译自英文(hono.dev)本页 27 段都还没译

模型写的

背景缺口主张方法结果分析局限

Best Practices

遵循最佳实践,而不是随意编写 Hono 应用

Hono is very flexible. You can write your app as you like. However, there are best practices that are better to follow.Hono is very flexible. However, there are best practices that are better to follow.Hono 很灵活,但有些最佳实践更好遵循 灵活性允许任意写法,但本页推荐能避免类型与结构问题的约定Hono 很灵活,但有些最佳实践更好遵循最佳实践

Don't make "Controllers" when possible

避免 RoR 式控制器,直接在路径定义后写处理函数

When possible, you should not create "Ruby on Rails-like Controllers".When possible, you should not create "Ruby on Rails-like Controllers".应尽量避免创建 Ruby on Rails 式控制器 后续说明这类控制器会破坏路径参数的类型推断应尽量避免创建 Ruby on Rails 式控制器Rails 式控制器

// 🙁
// A RoR-like Controller
const booksList = (c: Context) => {
  return c.json('list books')
}

app.get('/books', booksList)

The issue is related to types. For example, the path parameter cannot be inferred in the Controller without writing complex generics.The issue is related to types. For example, the path parameter cannot be inferred in the Controller without writing complex generics.控制器中路径参数需复杂泛型才能推断 仅有 Context 类型而缺少路由路径,c.req.param 无法得到具体类型控制器中路径参数需复杂泛型才能推断路径参数

// 🙁
// A RoR-like Controller
const bookPermalink = (c: Context) => {
  const id = c.req.param('id') // Can't infer the path param
  return c.json(`get ${id}`)
}

Therefore, you don't need to create RoR-like controllers and should write handlers directly after path definitions.Therefore, you don't need to create RoR-like controllers and should write handlers directly after path definitions.直接在路径定义后写处理函数,而非 RoR 式控制器 内联处理函数让 TypeScript 从路由字符串自动推断路径参数直接在路径定义后写处理函数,而非 RoR 式控制器处理函数

// 😃
app.get('/books/:id', (c) => {
  const id = c.req.param('id') // Can infer the path param
  return c.json(`get ${id}`)
})

factory.createHandlers() in hono/factory

使用 hono/factory 的 factory.createHandlers() 获得正确类型推断

If you still want to create a RoR-like Controller, use factory.createHandlers() in hono/factory. If you use this, type inference will work correctly.If you still want to create a RoR-like Controller, use factory.createHandlers() in hono/factory. If you use this, type inference will work correctly.hono/factory 的 factory.createHandlers() 为控制器保留类型推断 它把路由上下文贯穿中间件和处理函数,使路径参数保持类型hono/factory 的 factory.createHandlers() 为控制器保留类型推断factory.createHandlers()

import { createFactory } from 'hono/factory'
import { logger } from 'hono/logger'

// ...

// 😃
const factory = createFactory()

const middleware = factory.createMiddleware(async (c, next) => {
  c.set('foo', 'bar')
  await next()
})

const handlers = factory.createHandlers(logger(), middleware, (c) => {
  return c.json(c.var.foo)
})

app.get('/api', ...handlers)

Building a larger application

用 app.route() 把大型应用拆分为挂载的文件

Use app.route() to build a larger application without creating "Ruby on Rails-like Controllers".Use app.route() to build a larger application without creating "Ruby on Rails-like Controllers".用 app.route() 构建大型应用,无需 RoR 式控制器 挂载子应用让各文件处理函数保持内联,路径类型仍可推断用 app.route() 构建大型应用,无需 RoR 式控制器app.route()

If your application has /authors and /books endpoints and you wish to separate files from index.ts, create authors.ts and books.ts.If your application has /authors and /books endpoints and you wish to separate files from index.ts, create authors.ts and books.ts.把 /authors 和 /books 分别放到 authors.ts 与 books.ts 每个文件导出各自的 Hono 实例,包含该资源的全部路由把 /authors 和 /books 分别放到 authors.ts 与 books.ts端点

// authors.ts
import { Hono } from 'hono'

const app = new Hono()

app.get('/', (c) => c.json('list authors'))
app.post('/', (c) => c.json('create an author', 201))
app.get('/:id', (c) => c.json(`get ${c.req.param('id')}`))

export default app
// books.ts
import { Hono } from 'hono'

const app = new Hono()

app.get('/', (c) => c.json('list books'))
app.post('/', (c) => c.json('create a book', 201))
app.get('/:id', (c) => c.json(`get ${c.req.param('id')}`))

export default app

Then, import them and mount on the paths /authors and /books with app.route().Then, import them and mount on the paths /authors and /books with app.route().导入子应用并用 app.route() 挂载到对应路径 index.ts 保持精简,各资源各自维护路由定义导入子应用并用 app.route() 挂载到对应路径app.route()

// index.ts
import { Hono } from 'hono'
import authors from './authors'
import books from './books'

const app = new Hono()

// 😃
app.route('/authors', authors)
app.route('/books', books)

export default app

If you want to use RPC features

链式定义路由并导出 AppType,使 hc 获得正确 RPC 类型

The code above works well for normal use cases. However, if you want to use the RPC feature, you can get the correct type by chaining as follows.The code above works well for normal use cases. However, if you want to use the RPC feature, you can get the correct type by chaining as follows.链式定义路由,从导出的 AppType 获得正确 RPC 类型 链式调用在 typeof app 中保留完整路由结构,RPC 客户端需要它链式定义路由,从导出的 AppType 获得正确 RPC 类型RPC 类型

// authors.ts
import { Hono } from 'hono'

const app = new Hono()
  .get('/', (c) => c.json('list authors'))
  .post('/', (c) => c.json('create an author', 201))
  .get('/:id', (c) => c.json(`get ${c.req.param('id')}`))

export default app
export type AppType = typeof app

If you pass the type of the app to hc, it will get the correct type.If you pass the type of the app to hc, it will get the correct type.将 AppType 传给 hc,客户端即获得正确类型 客户端读取导出的路由结构推断请求与响应类型将 AppType 传给 hc,客户端即获得正确类型hc

import type { AppType } from './authors'
import { hc } from 'hono/client'

// 😃
const client = hc<AppType>('http://localhost') // Typed correctly

For more detailed information, please see the RPC page.

HEAD Request Best Practices

Understanding Hono's HEAD Handling

Hono 在路由匹配前把 HEAD 转为 GET 并去除响应体

Hono automatically handles HEAD requests by converting them to GET requests and stripping the response body. This behavior is built into the framework's dispatch layer and happens before route matching occurs.Hono automatically handles HEAD requests by converting them to GET requests and stripping the response body. This behavior is built into the framework's dispatch layer and happens before route matching occurs.Hono 把 HEAD 请求转为 GET 并去除响应体 该行为在路由匹配前的分发层完成,处理函数看到的是 GETHono 把 HEAD 请求转为 GET 并去除响应体HEAD 请求

✅ Do: Use GET Routes for HEAD Requests

// GOOD: This GET route automatically handles HEAD requests
app.get('/api/users', async (c) => {
  const users = await getUsers()
  c.header('X-Total-Count', users.length.toString())
  return c.json(users)
})

// HEAD /api/users will return:
// - Same headers as GET (including X-Total-Count)
// - Status 200
// - No body (null)

✅ Do: Use Middleware for HEAD-Specific Logic

// GOOD: Use middleware when HEAD needs different behavior
app.use('/api/resource', async (c, next) => {
  await next()

  // Add HEAD-specific headers after the handler
  if (c.req.method === 'HEAD') {
    c.header('X-HEAD-Processed', 'true')
    // Don't compute expensive body content for HEAD
    c.res = new Response(null, c.res)
  }
})

❌ Don't: Try to Create Dedicated HEAD Handlers

// BAD: This won't work as expected
app.head('/api/users', (c) => {
  // This handler will NEVER be called
  c.header('X-Custom', 'value')
  return c.text('ignored')
})

// BAD: Using on() also won't work
app.on('HEAD', '/api/users', (c) => {
  // Still converted to GET before route matching
})

Performance Considerations

HEAD 请求应跳过昂贵的响应体生成,依赖自动跳过

  • Avoid expensive operations in GET handlers if you expect many HEAD requests: Use middleware to detect HEAD and skip body generation
    Avoid expensive operations in GET handlers if you expect many HEAD requests: Use middleware to detect HEAD and skip body generation
    预计大量 HEAD 请求时,避免 GET 处理函数做昂贵运算 HEAD 仍会执行 GET 处理函数,生成用不到的响应体会浪费计算
    预计大量 HEAD 请求时,避免 GET 处理函数做昂贵运算
    GET 处理函数
  • Cache headers work identically: HEAD responses respect the same caching rules as GET
    Cache headers work identically: HEAD responses respect the same caching rules as GET
    HEAD 响应遵循与 GET 相同的缓存规则 HEAD 经由 GET 提供响应,因此相同的缓存响应头生效
    HEAD 响应遵循与 GET 相同的缓存规则
    缓存规则
  • Middleware compatibility: Most middleware works with HEAD, but body-processing middleware (like compression) automatically skips HEAD requests
    Middleware compatibility: Most middleware works with HEAD, but body-processing middleware (like compression) automatically skips HEAD requests
    压缩等处理响应体的中间件会自动跳过 HEAD 请求 跳过可避免压缩 HEAD 响应本就不携带的响应体
    压缩等处理响应体的中间件会自动跳过 HEAD 请求
    中间件兼容

Testing HEAD Requests

// Always test both GET and HEAD responses
it('handles HEAD requests correctly', async () => {
  const getRes = await app.request('/api/users')
  const headRes = await app.request('/api/users', { method: 'HEAD' })

  expect(headRes.status).toBe(getRes.status)
  expect(headRes.headers.get('X-Total-Count')).toBe(
    getRes.headers.get('X-Total-Count')
  )
  expect(headRes.body).toBe(null)
})

Notes

自动 HEAD 转换在所有运行时保持响应头一致

  • The automatic HEAD conversion ensures consistent headers between GET and HEAD responses
    The automatic HEAD conversion ensures consistent headers between GET and HEAD responses
    自动 HEAD 转换让 GET 与 HEAD 的响应头保持一致 两种方法执行同一处理函数,因此响应头值相同
    自动 HEAD 转换让 GET 与 HEAD 的响应头保持一致
    HEAD 转换
  • This behavior is consistent across all Hono runtimes (Cloudflare Workers, Deno, Bun, Node.js)
    This behavior is consistent across all Hono runtimes (Cloudflare Workers, Deno, Bun, Node.js)
    HEAD 行为在 Cloudflare Workers、Deno、Bun 和 Node.js 上一致 实现转换的分发层与运行时无关
    HEAD 行为在 Cloudflare Workers、Deno、Bun 和 Node.js 上一致
    HEAD 行为
  • If you need completely different logic for HEAD vs GET, consider using different endpoints rather than trying to override the framework's HEAD handling
    If you need completely different logic for HEAD vs GET, consider using different endpoints rather than trying to override the framework's HEAD handling
    需要完全不同的 HEAD 逻辑时,用独立端点而非覆盖框架行为 框架在路由前转换 HEAD,专用 HEAD 处理函数永远不会被调用
    需要完全不同的 HEAD 逻辑时,用独立端点而非覆盖框架行为
    HEAD 逻辑