本文へ移動

ベストプラクティス ​

Hono はとても柔軟で、好きな方法でアプリケーションを記述できます。 ただし、従うことが望ましいベストプラクティスがあります。

可能な限り「コントローラー」を作らない ​

可能な限り、「Ruby on Rails のようなコントローラー」を作らないようにしてください。

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

app.get('/books', booksList)

問題は型にあります。たとえば、複雑なジェネリクスを書かない限り、コントローラー内でパスパラメーターを推論できません。

ts
// 🙁
// 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}`)
}

そのため、RoR のようなコントローラーを作る必要はなく、パスの定義の直後にハンドラーを書くことを推奨します。

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

hono/factory の factory.createHandlers() ​

それでも RoR のようなコントローラーを作りたい場合は、hono/factory の factory.createHandlers() を使ってください。これを使うと、型推論が正しく機能します。

ts
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)

大規模なアプリケーションの構築 ​

「Ruby on Rails のようなコントローラー」を作らずに、app.route() で大規模なアプリケーションを構築します。

アプリケーションに /authors と /books のエンドポイントがあり、index.ts から別のファイルに分けたい場合は、authors.ts と books.ts を作成します。

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
ts
// 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

それらをインポートし、app.route() で /authors と /books のパスにマウントします。

ts
// 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

RPC 機能を使いたい場合 ​

上記のコードは通常の用途では問題なく動作します。 ただし、RPC 機能を使う場合は、次のようにメソッドを連結することで正しい型を取得できます。

ts
// 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

app の型を hc に渡すと、正しい型を取得できます。

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

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

詳細は RPC のページを参照してください。

HEAD リクエストのベストプラクティス ​

Hono の HEAD 処理の仕組み ​

Hono は、HEAD リクエストを GET リクエストに変換し、レスポンスボディを取り除くことで自動的に処理します。この動作はフレームワークのディスパッチ層に組み込まれており、ルートのマッチング前に行われます。

✅ 推奨:HEAD リクエストには GET ルートを使う ​

typescript
// 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)

✅ 推奨:HEAD 固有のロジックにはミドルウェアを使う ​

typescript
// 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)
  }
})

❌ 非推奨:専用の HEAD ハンドラーを作ろうとする ​

typescript
// 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
})

パフォーマンスに関する考慮事項 ​

  • HEAD リクエストが多いと予想される場合は、GET ハンドラーで負荷の高い処理を避ける:ミドルウェアで HEAD を検出し、ボディの生成をスキップします
  • キャッシュヘッダーの動作は同じ:HEAD レスポンスは GET と同じキャッシュ規則に従います
  • ミドルウェアの互換性:ほとんどのミドルウェアは HEAD に対応しますが、圧縮などのボディを処理するミドルウェアは HEAD リクエストを自動的にスキップします

HEAD リクエストのテスト ​

typescript
// 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)
})

注意点 ​

  • HEAD の自動変換によって、GET と HEAD のレスポンスヘッダーの一貫性が保たれます
  • この動作は、すべての Hono ランタイム(Cloudflare Workers、Deno、Bun、Node.js)で共通です
  • HEAD と GET でまったく異なるロジックが必要な場合は、フレームワークの HEAD 処理を上書きしようとする代わりに、別のエンドポイントを使うことを検討してください

MIT ライセンスで公開されています。