長年、JavaScript の Web フレームワーク選びはトレードオフの受け入れを意味してきました。Express は普遍的だが遅く Node に縛られる、Fastify は速いが Node 専用、Next.js は多機能だが重い。エッジランタイム——Cloudflare Workers、Deno Deploy、Bun、Vercel Edge——が登場すると、これらのフレームワークは限界を露呈しました。互換性のない依存、巨大なバンドル、Node の req/res に縛られた API。
Hono(日本語で「炎」🔥)は現代の答えです。Request、Response、fetch という Web Standards に完全に基づいて構築された 14KB 未満のフレームワークで、JavaScript ランタイムがあるところならどこでも動きます。同じコードが Cloudflare Workers、Bun、Deno、Node.js、Vercel、Netlify、AWS Lambda にデプロイされます——変更なしで。
なぜ Hono か
Hono は他の誰よりも 3 つのことを上手くやります。
- パフォーマンス。
RegExpRouterはすべてのルートパターンを単一の正規表現にコンパイルし、伝統的なルーターの線形ループを回避します。ベンチマークは秒間 40 万 ops を超え、Hono は JavaScript エコシステムで最速のルーターの一つです。 - 可搬性。 Web Standards は Node 依存ゼロを意味します。同じ
app.fetchが Cloudflare Worker でデフォルトエクスポートされ、Bun.serveに渡され、Deno サーバーにマウントされ、@hono/node-serverでアダプトされます。 - TypeScript ファーストの DX。 リテラル型として推論されるパスパラメータ、エンドツーエンド型安全な RPC クライアント、入出力型を推論するバリデータ。オートコンプリートはほぼテレパシーです。
はじめる
最速の方法は公式スターターで、選んだランタイム用にプロジェクトをスキャフォールディングします。
npm create hono@latest my-api cd my-api npm install npm run dev
スターターはどのテンプレートを使うか尋ねます:cloudflare-workers、bun、deno、nodejs、vercel、aws-lambda、nextjs など。適切な設定とデプロイスクリプトを選びます。すぐ試したい場合、単一ファイルからも始められます:
// src/index.ts import { Hono } from 'hono' const app = new Hono() app.get('/', (c) => c.text('Hello Hono!')) export default app
Cloudflare Workers ではこれで十分です。Bun では:Bun.serve({ fetch: app.fetch, port: 3000 })。Node では:@hono/node-server の serve({ fetch: app.fetch })。表面は同一です。
ルーティング
ルートは HTTP 動詞メソッドで宣言し、パラメータ、ワイルドカード、正規表現をサポートします。
import { Hono } from 'hono' const app = new Hono() app.get('/', (c) => c.text('Home')) app.get('/posts/:id', (c) => { const id = c.req.param('id') // string、型付き return c.json({ id }) }) app.get('/posts/:id/comments/:commentId', (c) => { const { id, commentId } = c.req.param() return c.json({ id, commentId }) }) app.get('/files/*', (c) => c.text('Wildcard')) app.post('/posts', async (c) => { const body = await c.req.json() return c.json({ created: body }, 201) })
パラメータはリテラル型として推論されます。TypeScript はパターンに :id を宣言した場合のみ c.req.param('id') が string を返すことを知っています。スペルミスはコンパイル時エラーです。
ルートのグルーピング
app.route() でサブアプリケーションをモジュールとして組み合わせ、それぞれにプレフィックスを持たせられます。
// routes/posts.ts import { Hono } from 'hono' const posts = new Hono() posts.get('/', (c) => c.json({ posts: [] })) posts.get('/:id', (c) => c.json({ id: c.req.param('id') })) export default posts // src/index.ts import { Hono } from 'hono' import posts from './routes/posts' const app = new Hono() app.route('/posts', posts)
ネストされたルートはルートアプリのベースパスと型を継承するので、RPC クライアントは構造全体を見られます。
Context オブジェクト
各ハンドラは c——現在のリクエストの Context——を受け取ります。入力を読み出力を生成するために学ぶ必要のある唯一の API です。
app.post('/echo', async (c) => { // 読み出し const userAgent = c.req.header('User-Agent') const page = c.req.query('page') const body = await c.req.json() const env = c.env // バインディング(Cloudflare の KV、D1、シークレット) // ミドルウェアとハンドラ間で共有される変数 c.set('requestId', crypto.randomUUID()) const id = c.get('requestId') // レスポンス c.header('X-Request-Id', id) c.status(200) return c.json({ userAgent, page, body, id }) })
主要なレスポンスメソッドは c.text、c.json、c.html、c.body(raw)、c.redirect。すべて第二引数にステータスコードを取ります。
ミドルウェア:オニオンモデル
ミドルウェアはハンドラの前後でコードを実行できる (c, next) => ... 関数です。組み合わせると古典的なオニオンモデルを形成します。最初に登録されたミドルウェアが最初に開始し、最後に終了します。
import { Hono } from 'hono' import { logger } from 'hono/logger' import { cors } from 'hono/cors' import { secureHeaders } from 'hono/secure-headers' const app = new Hono() app.use('*', logger()) app.use('*', secureHeaders()) app.use('/api/*', cors({ origin: 'https://spinny.dev' })) app.use('*', async (c, next) => { const start = performance.now() await next() c.header('X-Response-Time', `${performance.now() - start}ms`) })
await next() は次のミドルウェアに制御を渡します。それ以降のコードは、ハンドラが結果を生成した後のレスポンスフェーズで実行されます。next() の前に Response を返すとチェーンを短絡します。
組み込みミドルウェア
Hono は本番対応のミドルウェアを豊富に同梱し、hono/... からインポートできます:
| ミドルウェア | 役割 |
|---|---|
logger | メソッド、パス、ステータス、所要時間の構造化ログ |
cors | origin、メソッド、ヘッダで設定可能な CORS |
csrf | origin ベースの CSRF 保護 |
secureHeaders | CSP、HSTS、X-Frame-Options を設定 |
bearerAuth / basicAuth | すぐ使える Bearer/Basic 認証 |
jwt | jose で JWT を検証/署名 |
etag | ETag を生成し 304 を処理 |
cache | Web Cache API でのキャッシュ |
compress | レスポンスの gzip/deflate |
bodyLimit | 閾値を超えるボディを拒否 |
timing | プロファイリング用 Server-Timing ヘッダ |
型安全なカスタムミドルウェア
Context を型付き変数で拡張するには createMiddleware を使います:
import { createMiddleware } from 'hono/factory' type AuthVars = { userId: string; role: 'user' | 'admin' } export const requireAuth = createMiddleware<{ Variables: AuthVars }>( async (c, next) => { const token = c.req.header('Authorization')?.replace('Bearer ', '') if (!token) return c.json({ error: 'Unauthorized' }, 401) const payload = await verifyJwt(token) c.set('userId', payload.sub) c.set('role', payload.role) await next() } ) // 使用 app.get('/me', requireAuth, (c) => { const userId = c.var.userId // string、型付き return c.json({ userId }) })
ミドルウェアの下流では c.var.userId がキャストなしで型付けされます。これはチェーン全体に伝播します。
Zod でのバリデーション
@hono/zod-validator は Zod をリクエストサイクルに組み込みます。スキーマを定義し、ルートに適用すると、すでに検証済みで型付けされた入力が得られます。
import { Hono } from 'hono' import { zValidator } from '@hono/zod-validator' import { z } from 'zod' const createPost = z.object({ title: z.string().min(1).max(200), body: z.string().min(1), tags: z.array(z.string()).default([]), }) app.post( '/posts', zValidator('json', createPost), (c) => { const data = c.req.valid('json') // スキーマで型付け return c.json({ ok: true, post: data }, 201) } )
ボディがバリデーションに失敗すると、Hono はハンドラを呼ぶ前に Zod エラー付きで 400 を返します。query、param、header、cookie、form も検証できます。
RPC:エンドツーエンド型安全なクライアント
Hono を真に他と差別化する機能が RPC モード です。サーバーからアプリの型をエクスポートし、hc クライアントがインポートすると、パス、クエリ、ボディ、ヘッダ、レスポンスを含む完全なオートコンプリートが得られます——コード生成や OpenAPI なしで。
// server.ts import { Hono } from 'hono' import { zValidator } from '@hono/zod-validator' import { z } from 'zod' const app = new Hono() .get('/posts/:id', (c) => c.json({ id: c.req.param('id'), title: 'Hello' }) ) .post( '/posts', zValidator('json', z.object({ title: z.string(), body: z.string() })), (c) => c.json({ ok: true }, 201) ) export type AppType = typeof app export default app
// client.ts import { hc } from 'hono/client' import type { AppType } from './server' const client = hc<AppType>('https://api.spinny.dev') const res = await client.posts[':id'].$get({ param: { id: '42' } }) if (res.ok) { const data = await res.json() // { id: string, title: string } console.log(data.title) } const created = await client.posts.$post({ json: { title: 'こんにちは', body: 'Hono は炎' }, // 検証済み })
サーバーでルート名を変更すると、CI でクライアントの TypeScript が即座に壊れます。tRPC と同じ利点ですが、標準 HTTP 上で、特定のミドルウェアなしに、極小バンドルで実現します。
ステータスコードによる判別
異なるステータスを返すと、クライアントが自動で判別します。
.get('/posts/:id', (c) => { const post = findPost(c.req.param('id')) if (!post) return c.json({ error: 'not found' }, 404) return c.json({ post }, 200) })
const res = await client.posts[':id'].$get({ param: { id } }) if (res.status === 404) { const { error } = await res.json() // { error: string } } if (res.status === 200) { const { post } = await res.json() // { post: Post } }
ルーターとパフォーマンス
Hono は異なるトレードオフを持つ 5 つのルーターを提供します。デフォルトは SmartRouter で、起動時にどのルーターがルートに最適か測定し、それに固定します。
| ルーター | 強み | いつ使うか |
|---|---|---|
RegExpRouter | 最高速度、コンパイルされた正規表現 | ほとんどの API のデフォルト |
TrieRouter | あらゆるパターンをサポート | RegExp で扱えない複雑なパターン |
SmartRouter | 自動で最適を選択 | 推奨デフォルト |
LinearRouter | 超高速登録 | ワンショットワーカー、コールドスタートが重要 |
PatternRouter | 最小バンドル(<15KB) | 極端なサイズ制約 |
コールドスタートが頻繁なステートレスワーカーには、LinearRouter が初期コンパイルコストを回避し、登録とマッチングを測定すると find-my-way より 33 倍速いです。
import { Hono } from 'hono' import { LinearRouter } from 'hono/router/linear-router' const app = new Hono({ router: new LinearRouter() })
マルチランタイムデプロイ
同じ export default app がエントリファイルを変えるだけでターゲットを切り替えます。
Cloudflare Workers
// src/index.ts import { Hono } from 'hono' type Bindings = { MY_KV: KVNamespace; DB: D1Database } const app = new Hono<{ Bindings: Bindings }>() app.get('/cache/:key', async (c) => { const value = await c.env.MY_KV.get(c.req.param('key')) return c.json({ value }) }) export default app
デプロイ:npx wrangler deploy。KV/D1/R2/Queues バインディングはネイティブ。
Bun
import { Hono } from 'hono' const app = new Hono() app.get('/', (c) => c.text('Bun + Hono')) Bun.serve({ fetch: app.fetch, port: 3000 })
Node.js
import { serve } from '@hono/node-server' import { Hono } from 'hono' const app = new Hono() app.get('/', (c) => c.text('Node + Hono')) serve({ fetch: app.fetch, port: 3000 })
Deno
import { Hono } from 'jsr:@hono/hono' const app = new Hono() app.get('/', (c) => c.text('Deno + Hono')) Deno.serve(app.fetch)
Vercel
// api/[[...route]].ts import { Hono } from 'hono' import { handle } from 'hono/vercel' const app = new Hono().basePath('/api') app.get('/hello', (c) => c.json({ msg: 'Hello from Vercel' })) export const GET = handle(app) export const POST = handle(app)
同じビジネスコード、同じルーティング、同じミドルウェア。アダプタだけが変わります。
実践例:認証と DB を備えた REST API
すべてを組み合わせます。Cloudflare Workers + D1 上のブログ API、JWT 認証、バリデーション、RPC 付き。
import { Hono } from 'hono' import { jwt } from 'hono/jwt' import { logger } from 'hono/logger' import { cors } from 'hono/cors' import { zValidator } from '@hono/zod-validator' import { z } from 'zod' type Bindings = { DB: D1Database; JWT_SECRET: string } type Variables = { jwtPayload: { sub: string } } const app = new Hono<{ Bindings: Bindings; Variables: Variables }>() app.use('*', logger()) app.use('/api/*', cors({ origin: 'https://spinny.dev', credentials: true })) const auth = (c: any, next: any) => jwt({ secret: c.env.JWT_SECRET })(c, next) const api = app.basePath('/api') api.get('/posts', async (c) => { const { results } = await c.env.DB .prepare('SELECT id, title, created_at FROM posts ORDER BY created_at DESC LIMIT 50') .all() return c.json({ posts: results }) }) api.post( '/posts', auth, zValidator('json', z.object({ title: z.string().min(1).max(200), body: z.string().min(1), })), async (c) => { const { title, body } = c.req.valid('json') const userId = c.var.jwtPayload.sub const result = await c.env.DB .prepare('INSERT INTO posts (title, body, author_id) VALUES (?, ?, ?) RETURNING id') .bind(title, body, userId) .first<{ id: number }>() return c.json({ id: result?.id }, 201) } ) api.onError((err, c) => { console.error(err) return c.json({ error: 'Internal error' }, 500) }) export type AppType = typeof api export default app
React クライアントは完全な型安全性で消費します:
import { hc } from 'hono/client' import type { AppType } from '../api/src/index' const api = hc<AppType>(import.meta.env.VITE_API_URL) const res = await api.posts.$post({ json: { title: '2026 年の Hono', body: '...' }, }, { headers: { Authorization: `Bearer ${token}` }, })
テスト
app.request() は HTTP サーバーを立ち上げずにルートをテストできます。本番で実行するのと同じパスがメモリ内で実行されます。
import { describe, it, expect } from 'vitest' import app from '../src/index' describe('GET /api/posts', () => { it('投稿のリストを返す', async () => { const res = await app.request('/api/posts') expect(res.status).toBe(200) const body = await res.json() expect(body.posts).toBeInstanceOf(Array) }) })
Cloudflare Workers では、@cloudflare/vitest-pool-workers がモックバインディング付きの実 Worker 内で同じテストを実行します——最大限のリアリズム、ゼロデプロイ。
ベストプラクティス
1. ルート定義をチェーンする
const app = new Hono() .get('/posts', handler1) .post('/posts', handler2) .get('/posts/:id', handler3)
チェーンはルートごとに app の型を最新に保ち、RPC クライアントに必須です。app.get(...) を別の行で個別に定義すると推論が壊れます。
2. 実装ではなく型をエクスポート
クライアントはアプリではなく AppType をインポートする必要があります。import type でフロントエンドビルドにバックエンドコードが含まれないことを保証します。
3. ドメインごとに 1 つのルーター
posts 用、users 用、webhooks 用のサブアプリ。app.route() で組み合わせ、それぞれが自身のミドルウェアを持ちます。構造はメガファイルなしにスケールします。
4. 常に境界でバリデーション
すべての外部入力(ボディ、クエリ、ヘッダ)は zValidator を通る必要があります。下流のデータを信用しないこと。ランタイム検証なしの TypeScript キャストは、待機中のバグです。
5. グローバルクライアントではなくバインディングに頼る
Cloudflare では c.env 経由で KV/D1/R2 にアクセス。グローバルシングルトンなし、Worker 間で持続する接続なし。ステートレスモデルは機能であって制約ではありません。
6. ルーターを最適化する前に測定
デフォルトの SmartRouter は 95% のケースで問題ありません。プロファイリングで実際のボトルネックを見たときだけルーターを切り替えます。
結論
Hono は 2026 年、TypeScript でエッジ対応 API を構築するためのデファクトスタンダードになりました。Web Standards、パフォーマンス、型安全性、可搬性の組み合わせは、伝統的なフレームワークを足止めしてきた問題——ランタイムロックイン、重いバンドル、クライアントとサーバー間の脆弱な型システム——をまさに解決します。
Cloudflare Workers でのマイクロサービス、Node モノリスでの Express の置き換え、Vercel での関数、Bun での API に使うでしょう。同じ知識がどこへでも転用可能です——だからこそ今後数年の堅実な投資なのです。
開始チェックリスト:
npm create hono@latestでランタイムテンプレートを選択- チェーンでルートを定義(
.get(...).post(...))- グローバルミドルウェアとして
logger、cors、secureHeadersを追加@hono/zod-validatorですべての入力を検証AppTypeをエクスポートし、型安全なhcクライアントで API を消費- HTTP サーバー不要の
app.request()でテストを書くwrangler deploy(CF)、vercel deploy、ランタイムのバンドラでデプロイ