hono-routing について
Hono 開発者向けのルーティングとミドルウェアのスキルです。ルート構成、リクエスト検証、型安全なコンテキスト、ストリーミング応答を扱い、ランタイム固有の注意点も説明します。
主な機能
- 01ルートとサブアプリの構成
- 02組み込み・カスタムミドルウェアの設定
- 03型推論を伴うリクエスト検証
- 04型安全なコンテキストの拡張
- 05ストリーミング応答と WebSocket の処理
活用シーン
- 01パラメーター制約付き API ルートの構築
- 02リクエスト本文、クエリ、ヘッダーの検証
- 03特定ルートへの認証・セキュリティミドルウェア適用
- 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.jsonreferences/middleware-catalog.mdrpc-guide.mdtop-errors.mdvalidation-libraries.mdrules/hono-routing.mdscripts/check-versions.shtemplates/context-extension.tserror-handling.tsmiddleware-composition.tspackage.jsonrouting-patterns.tsrpc-client.tsrpc-pattern.tsvalidation-valibot.tsvalidation-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()は使用しません) - ランタイム(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/wsではなく、hono/cloudflare-workersからインポートします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'],
}))
セキュリティミドルウェアのオプション:
| ミドルウェア | 目的 |
|---|---|
secureHeaders | X-Frame-Options、CSP、HSTS、XSS 対策 |
csrf | Origin/Sec-Fetch-Site の検証による CSRF 対策 |
bearerAuth | ベアラートークン認証 |
basicAuth | HTTP Basic 認証 |
ipRestriction | IP の許可リスト/ブロックリスト |
ミドルウェアの組み合わせ
条件ロジックを使ってミドルウェアを合成します。
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 })
})
重要:
- 型安全な
c.get()のためにVariables型を定義します - 環境変数(Cloudflare Workers 用)には
Bindings型を定義します - ミドルウェアでは
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(Ahead-of-Time)コンパイル
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
重要:
- RPC の型推論には、
const route = app.get(...)を必ず使用してください 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)には使用せず、代わりに
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
}
})
Not Found ハンドラー
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 から複雑な型がインスタンス化されます。omit、extend、pick などの Zod メソッドによって、さらに悪化します。
防止策:アプリ全体ではなく、特定のルートグループをエクスポートします
// ❌ Slow
export type AppType = typeof app
// ✅ Fast
const userRoutes = app.get(...).post(...)
export type UserRoutes = typeof userRoutes
大規模アプリ向けの高度な回避策(ルートが 100 以上の場合):
- モノレポのライブラリに分割する:
// 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
-
ビルド設定を分ける:
- 本番環境:
.d.tsを生成する完全なtsc(RPC クライアント向け) - 開発環境:メインルーターでは
tscを省略し、サブルーターのみ型チェックする(ライブリロードが高速化されます)
- 本番環境:
-
パフォーマンスを低下させる Zod メソッドを避ける:
z.omit()、z.extend()、z.pick()- 言語サーバーの負荷が 10 倍に増加します- 可能な場合は、交差型ではなくインターフェースを使用します
問題 #2:ミドルウェアのレスポンスが RPC で型付けされない
エラー:ミドルウェアのレスポンス(notFound() や onError() を含む)が RPC クライアントで推論されない
出典: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 の誤用
エラー:HTTPException ではなく、通常の Error をスローする
出典:公式ドキュメント
発生理由: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
発生理由: JWT ヘッダーの改ざんを防止するため、v4.11.4 のセキュリティ修正により、アルゴリズムの明示的な指定が必要になりました
予防策: アルゴリズムパラメーターは必ず指定してください
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 のキャッシュを迂回し、ボディストリームを消費します
予防策: raw リクエストにアクセスする代わりに、必ず 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 のエラーと解決方法
公式ドキュメント
- Hono: https://hono.dev
- Hono Routing: https://hono.dev/docs/api/routing
- Hono Middleware: https://hono.dev/docs/guides/middleware
- Hono Validation: https://hono.dev/docs/guides/validation
- Hono RPC: https://hono.dev/docs/guides/rpc
- Hono Context: https://hono.dev/docs/api/context
- Context7 Library ID:
/llmstxt/hono_dev_llms-full_txt
依存関係(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: テストスイート全体に合格
このスキルのすべてのパターンは、本番環境で検証済みです。
質問や問題がありますか?
- まず
references/top-errors.mdを確認してください - セットアップのすべての手順を確認してください
- ミドルウェアで
await next()が呼び出されていることを確認してください - RPC ルートで
const route = app.get(...)パターンが使われていることを確認してください - 公式ドキュメントを確認してください: https://hono.dev