spinny:~/writing $ less hono-framework-guide.md
12長年、JavaScript の Web フレームワーク選びはトレードオフの受け入れを意味してきました。Express は普遍的だが遅く Node に縛られる、Fastify は速いが Node 専用、Next.js は多機能だが重い。エッジランタイム——Cloudflare Workers、Deno Deploy、Bun、Vercel Edge——が登場すると、これらのフレームワークは限界を露呈しました。互換性のない依存、巨大なバンドル、Node の `req`/`res` に縛られた API。34[Hono](https://hono.dev)(日本語で「炎」🔥)は現代の答えです。`Request`、`Response`、`fetch` という Web Standards に完全に基づいて構築された 14KB 未満のフレームワークで、JavaScript ランタイムがあるところならどこでも動きます。同じコードが Cloudflare Workers、Bun、Deno、Node.js、Vercel、Netlify、AWS Lambda にデプロイされます——変更なしで。56## なぜ Hono か78Hono は他の誰よりも 3 つのことを上手くやります。9101. **パフォーマンス。** `RegExpRouter` はすべてのルートパターンを単一の正規表現にコンパイルし、伝統的なルーターの線形ループを回避します。ベンチマークは秒間 40 万 ops を超え、Hono は JavaScript エコシステムで最速のルーターの一つです。112. **可搬性。** Web Standards は Node 依存ゼロを意味します。同じ `app.fetch` が Cloudflare Worker でデフォルトエクスポートされ、`Bun.serve` に渡され、Deno サーバーにマウントされ、`@hono/node-server` でアダプトされます。123. **TypeScript ファーストの DX。** リテラル型として推論されるパスパラメータ、エンドツーエンド型安全な RPC クライアント、入出力型を推論するバリデータ。オートコンプリートはほぼテレパシーです。1314```mermaid15graph LR16 Client[Client] -->|Request| App[app.fetch]17 App --> MW1[Middleware 1]18 MW1 --> MW2[Middleware 2]19 MW2 --> Router[RegExpRouter]20 Router --> Handler[Route Handler]21 Handler --> Context[c.json / c.text]22 Context -->|Response| Client23 App -.->|deploy| CF[Cloudflare Workers]24 App -.->|deploy| Bun[Bun]25 App -.->|deploy| Deno[Deno]26 App -.->|deploy| Node[Node.js]27 App -.->|deploy| Vercel[Vercel]28```2930## はじめる3132最速の方法は公式スターターで、選んだランタイム用にプロジェクトをスキャフォールディングします。3334```bash35npm create hono@latest my-api36cd my-api37npm install38npm run dev39```4041スターターはどのテンプレートを使うか尋ねます:`cloudflare-workers`、`bun`、`deno`、`nodejs`、`vercel`、`aws-lambda`、`nextjs` など。適切な設定とデプロイスクリプトを選びます。すぐ試したい場合、単一ファイルからも始められます:4243```typescript44// src/index.ts45import { Hono } from 'hono'4647const app = new Hono()4849app.get('/', (c) => c.text('Hello Hono!'))5051export default app52```5354Cloudflare Workers ではこれで十分です。Bun では:`Bun.serve({ fetch: app.fetch, port: 3000 })`。Node では:`@hono/node-server` の `serve({ fetch: app.fetch })`。表面は同一です。5556## ルーティング5758ルートは HTTP 動詞メソッドで宣言し、パラメータ、ワイルドカード、正規表現をサポートします。5960```typescript61import { Hono } from 'hono'6263const app = new Hono()6465app.get('/', (c) => c.text('Home'))66app.get('/posts/:id', (c) => {67 const id = c.req.param('id') // string、型付き68 return c.json({ id })69})70app.get('/posts/:id/comments/:commentId', (c) => {71 const { id, commentId } = c.req.param()72 return c.json({ id, commentId })73})74app.get('/files/*', (c) => c.text('Wildcard'))75app.post('/posts', async (c) => {76 const body = await c.req.json()77 return c.json({ created: body }, 201)78})79```8081パラメータはリテラル型として推論されます。TypeScript はパターンに `:id` を宣言した場合のみ `c.req.param('id')` が `string` を返すことを知っています。スペルミスはコンパイル時エラーです。8283### ルートのグルーピング8485`app.route()` でサブアプリケーションをモジュールとして組み合わせ、それぞれにプレフィックスを持たせられます。8687```typescript88// routes/posts.ts89import { Hono } from 'hono'9091const posts = new Hono()92posts.get('/', (c) => c.json({ posts: [] }))93posts.get('/:id', (c) => c.json({ id: c.req.param('id') }))94export default posts9596// src/index.ts97import { Hono } from 'hono'98import posts from './routes/posts'99100const app = new Hono()101app.route('/posts', posts)102```103104ネストされたルートはルートアプリのベースパスと型を継承するので、RPC クライアントは構造全体を見られます。105106## Context オブジェクト107108各ハンドラは `c`——現在のリクエストの Context——を受け取ります。入力を読み出力を生成するために学ぶ必要のある唯一の API です。109110```typescript111app.post('/echo', async (c) => {112 // 読み出し113 const userAgent = c.req.header('User-Agent')114 const page = c.req.query('page')115 const body = await c.req.json()116 const env = c.env // バインディング(Cloudflare の KV、D1、シークレット)117118 // ミドルウェアとハンドラ間で共有される変数119 c.set('requestId', crypto.randomUUID())120 const id = c.get('requestId')121122 // レスポンス123 c.header('X-Request-Id', id)124 c.status(200)125 return c.json({ userAgent, page, body, id })126})127```128129主要なレスポンスメソッドは `c.text`、`c.json`、`c.html`、`c.body`(raw)、`c.redirect`。すべて第二引数にステータスコードを取ります。130131## ミドルウェア:オニオンモデル132133ミドルウェアはハンドラの前後でコードを実行できる `(c, next) => ...` 関数です。組み合わせると古典的なオニオンモデルを形成します。最初に登録されたミドルウェアが最初に開始し、最後に終了します。134135```typescript136import { Hono } from 'hono'137import { logger } from 'hono/logger'138import { cors } from 'hono/cors'139import { secureHeaders } from 'hono/secure-headers'140141const app = new Hono()142143app.use('*', logger())144app.use('*', secureHeaders())145app.use('/api/*', cors({ origin: 'https://spinny.dev' }))146147app.use('*', async (c, next) => {148 const start = performance.now()149 await next()150 c.header('X-Response-Time', `${performance.now() - start}ms`)151})152```153154`await next()` は次のミドルウェアに制御を渡します。それ以降のコードは、ハンドラが結果を生成した後のレスポンスフェーズで実行されます。`next()` の前に `Response` を返すとチェーンを短絡します。155156### 組み込みミドルウェア157158Hono は本番対応のミドルウェアを豊富に同梱し、`hono/...` からインポートできます:159160| ミドルウェア | 役割 |161|-----------|---------|162| `logger` | メソッド、パス、ステータス、所要時間の構造化ログ |163| `cors` | origin、メソッド、ヘッダで設定可能な CORS |164| `csrf` | origin ベースの CSRF 保護 |165| `secureHeaders` | CSP、HSTS、X-Frame-Options を設定 |166| `bearerAuth` / `basicAuth` | すぐ使える Bearer/Basic 認証 |167| `jwt` | `jose` で JWT を検証/署名 |168| `etag` | ETag を生成し 304 を処理 |169| `cache` | Web Cache API でのキャッシュ |170| `compress` | レスポンスの gzip/deflate |171| `bodyLimit` | 閾値を超えるボディを拒否 |172| `timing` | プロファイリング用 Server-Timing ヘッダ |173174### 型安全なカスタムミドルウェア175176`Context` を型付き変数で拡張するには `createMiddleware` を使います:177178```typescript179import { createMiddleware } from 'hono/factory'180181type AuthVars = { userId: string; role: 'user' | 'admin' }182183export const requireAuth = createMiddleware<{ Variables: AuthVars }>(184 async (c, next) => {185 const token = c.req.header('Authorization')?.replace('Bearer ', '')186 if (!token) return c.json({ error: 'Unauthorized' }, 401)187188 const payload = await verifyJwt(token)189 c.set('userId', payload.sub)190 c.set('role', payload.role)191 await next()192 }193)194195// 使用196app.get('/me', requireAuth, (c) => {197 const userId = c.var.userId // string、型付き198 return c.json({ userId })199})200```201202ミドルウェアの下流では `c.var.userId` がキャストなしで型付けされます。これはチェーン全体に伝播します。203204## Zod でのバリデーション205206`@hono/zod-validator` は Zod をリクエストサイクルに組み込みます。スキーマを定義し、ルートに適用すると、すでに検証済みで型付けされた入力が得られます。207208```typescript209import { Hono } from 'hono'210import { zValidator } from '@hono/zod-validator'211import { z } from 'zod'212213const createPost = z.object({214 title: z.string().min(1).max(200),215 body: z.string().min(1),216 tags: z.array(z.string()).default([]),217})218219app.post(220 '/posts',221 zValidator('json', createPost),222 (c) => {223 const data = c.req.valid('json') // スキーマで型付け224 return c.json({ ok: true, post: data }, 201)225 }226)227```228229ボディがバリデーションに失敗すると、Hono はハンドラを呼ぶ前に Zod エラー付きで 400 を返します。`query`、`param`、`header`、`cookie`、`form` も検証できます。230231## RPC:エンドツーエンド型安全なクライアント232233Hono を真に他と差別化する機能が **RPC モード** です。サーバーからアプリの型をエクスポートし、`hc` クライアントがインポートすると、パス、クエリ、ボディ、ヘッダ、レスポンスを含む完全なオートコンプリートが得られます——コード生成や OpenAPI なしで。234235```typescript236// server.ts237import { Hono } from 'hono'238import { zValidator } from '@hono/zod-validator'239import { z } from 'zod'240241const app = new Hono()242 .get('/posts/:id', (c) =>243 c.json({ id: c.req.param('id'), title: 'Hello' })244 )245 .post(246 '/posts',247 zValidator('json', z.object({ title: z.string(), body: z.string() })),248 (c) => c.json({ ok: true }, 201)249 )250251export type AppType = typeof app252export default app253```254255```typescript256// client.ts257import { hc } from 'hono/client'258import type { AppType } from './server'259260const client = hc<AppType>('https://api.spinny.dev')261262const res = await client.posts[':id'].$get({ param: { id: '42' } })263if (res.ok) {264 const data = await res.json() // { id: string, title: string }265 console.log(data.title)266}267268const created = await client.posts.$post({269 json: { title: 'こんにちは', body: 'Hono は炎' }, // 検証済み270})271```272273サーバーでルート名を変更すると、CI でクライアントの TypeScript が即座に壊れます。tRPC と同じ利点ですが、標準 HTTP 上で、特定のミドルウェアなしに、極小バンドルで実現します。274275### ステータスコードによる判別276277異なるステータスを返すと、クライアントが自動で判別します。278279```typescript280.get('/posts/:id', (c) => {281 const post = findPost(c.req.param('id'))282 if (!post) return c.json({ error: 'not found' }, 404)283 return c.json({ post }, 200)284})285```286287```typescript288const res = await client.posts[':id'].$get({ param: { id } })289if (res.status === 404) {290 const { error } = await res.json() // { error: string }291}292if (res.status === 200) {293 const { post } = await res.json() // { post: Post }294}295```296297## ルーターとパフォーマンス298299Hono は異なるトレードオフを持つ 5 つのルーターを提供します。デフォルトは `SmartRouter` で、起動時にどのルーターがルートに最適か測定し、それに固定します。300301| ルーター | 強み | いつ使うか |302|--------|-----------|-------------|303| `RegExpRouter` | 最高速度、コンパイルされた正規表現 | ほとんどの API のデフォルト |304| `TrieRouter` | あらゆるパターンをサポート | RegExp で扱えない複雑なパターン |305| `SmartRouter` | 自動で最適を選択 | 推奨デフォルト |306| `LinearRouter` | 超高速登録 | ワンショットワーカー、コールドスタートが重要 |307| `PatternRouter` | 最小バンドル(<15KB) | 極端なサイズ制約 |308309コールドスタートが頻繁なステートレスワーカーには、`LinearRouter` が初期コンパイルコストを回避し、登録とマッチングを測定すると `find-my-way` より 33 倍速いです。310311```typescript312import { Hono } from 'hono'313import { LinearRouter } from 'hono/router/linear-router'314315const app = new Hono({ router: new LinearRouter() })316```317318## マルチランタイムデプロイ319320同じ `export default app` がエントリファイルを変えるだけでターゲットを切り替えます。321322### Cloudflare Workers323324```typescript325// src/index.ts326import { Hono } from 'hono'327328type Bindings = { MY_KV: KVNamespace; DB: D1Database }329const app = new Hono<{ Bindings: Bindings }>()330331app.get('/cache/:key', async (c) => {332 const value = await c.env.MY_KV.get(c.req.param('key'))333 return c.json({ value })334})335336export default app337```338339デプロイ:`npx wrangler deploy`。KV/D1/R2/Queues バインディングはネイティブ。340341### Bun342343```typescript344import { Hono } from 'hono'345const app = new Hono()346app.get('/', (c) => c.text('Bun + Hono'))347348Bun.serve({ fetch: app.fetch, port: 3000 })349```350351### Node.js352353```typescript354import { serve } from '@hono/node-server'355import { Hono } from 'hono'356357const app = new Hono()358app.get('/', (c) => c.text('Node + Hono'))359360serve({ fetch: app.fetch, port: 3000 })361```362363### Deno364365```typescript366import { Hono } from 'jsr:@hono/hono'367const app = new Hono()368app.get('/', (c) => c.text('Deno + Hono'))369Deno.serve(app.fetch)370```371372### Vercel373374```typescript375// api/[[...route]].ts376import { Hono } from 'hono'377import { handle } from 'hono/vercel'378379const app = new Hono().basePath('/api')380app.get('/hello', (c) => c.json({ msg: 'Hello from Vercel' }))381382export const GET = handle(app)383export const POST = handle(app)384```385386同じビジネスコード、同じルーティング、同じミドルウェア。アダプタだけが変わります。387388## 実践例:認証と DB を備えた REST API389390すべてを組み合わせます。Cloudflare Workers + D1 上のブログ API、JWT 認証、バリデーション、RPC 付き。391392```typescript393import { Hono } from 'hono'394import { jwt } from 'hono/jwt'395import { logger } from 'hono/logger'396import { cors } from 'hono/cors'397import { zValidator } from '@hono/zod-validator'398import { z } from 'zod'399400type Bindings = { DB: D1Database; JWT_SECRET: string }401type Variables = { jwtPayload: { sub: string } }402403const app = new Hono<{ Bindings: Bindings; Variables: Variables }>()404405app.use('*', logger())406app.use('/api/*', cors({ origin: 'https://spinny.dev', credentials: true }))407408const auth = (c: any, next: any) =>409 jwt({ secret: c.env.JWT_SECRET })(c, next)410411const api = app.basePath('/api')412413api.get('/posts', async (c) => {414 const { results } = await c.env.DB415 .prepare('SELECT id, title, created_at FROM posts ORDER BY created_at DESC LIMIT 50')416 .all()417 return c.json({ posts: results })418})419420api.post(421 '/posts',422 auth,423 zValidator('json', z.object({424 title: z.string().min(1).max(200),425 body: z.string().min(1),426 })),427 async (c) => {428 const { title, body } = c.req.valid('json')429 const userId = c.var.jwtPayload.sub430 const result = await c.env.DB431 .prepare('INSERT INTO posts (title, body, author_id) VALUES (?, ?, ?) RETURNING id')432 .bind(title, body, userId)433 .first<{ id: number }>()434 return c.json({ id: result?.id }, 201)435 }436)437438api.onError((err, c) => {439 console.error(err)440 return c.json({ error: 'Internal error' }, 500)441})442443export type AppType = typeof api444export default app445```446447React クライアントは完全な型安全性で消費します:448449```typescript450import { hc } from 'hono/client'451import type { AppType } from '../api/src/index'452453const api = hc<AppType>(import.meta.env.VITE_API_URL)454455const res = await api.posts.$post({456 json: { title: '2026 年の Hono', body: '...' },457}, {458 headers: { Authorization: `Bearer ${token}` },459})460```461462## テスト463464`app.request()` は HTTP サーバーを立ち上げずにルートをテストできます。本番で実行するのと同じパスがメモリ内で実行されます。465466```typescript467import { describe, it, expect } from 'vitest'468import app from '../src/index'469470describe('GET /api/posts', () => {471 it('投稿のリストを返す', async () => {472 const res = await app.request('/api/posts')473 expect(res.status).toBe(200)474 const body = await res.json()475 expect(body.posts).toBeInstanceOf(Array)476 })477})478```479480Cloudflare Workers では、`@cloudflare/vitest-pool-workers` がモックバインディング付きの実 Worker 内で同じテストを実行します——最大限のリアリズム、ゼロデプロイ。481482## ベストプラクティス483484### 1. ルート定義をチェーンする485486```typescript487const app = new Hono()488 .get('/posts', handler1)489 .post('/posts', handler2)490 .get('/posts/:id', handler3)491```492493チェーンはルートごとに `app` の型を最新に保ち、RPC クライアントに必須です。`app.get(...)` を別の行で個別に定義すると推論が壊れます。494495### 2. 実装ではなく型をエクスポート496497クライアントはアプリではなく `AppType` をインポートする必要があります。`import type` でフロントエンドビルドにバックエンドコードが含まれないことを保証します。498499### 3. ドメインごとに 1 つのルーター500501`posts` 用、`users` 用、`webhooks` 用のサブアプリ。`app.route()` で組み合わせ、それぞれが自身のミドルウェアを持ちます。構造はメガファイルなしにスケールします。502503### 4. 常に境界でバリデーション504505すべての外部入力(ボディ、クエリ、ヘッダ)は `zValidator` を通る必要があります。下流のデータを信用しないこと。ランタイム検証なしの TypeScript キャストは、待機中のバグです。506507### 5. グローバルクライアントではなくバインディングに頼る508509Cloudflare では `c.env` 経由で KV/D1/R2 にアクセス。グローバルシングルトンなし、Worker 間で持続する接続なし。ステートレスモデルは機能であって制約ではありません。510511### 6. ルーターを最適化する前に測定512513デフォルトの `SmartRouter` は 95% のケースで問題ありません。プロファイリングで実際のボトルネックを見たときだけルーターを切り替えます。514515## 結論516517Hono は 2026 年、TypeScript でエッジ対応 API を構築するためのデファクトスタンダードになりました。Web Standards、パフォーマンス、型安全性、可搬性の組み合わせは、伝統的なフレームワークを足止めしてきた問題——ランタイムロックイン、重いバンドル、クライアントとサーバー間の脆弱な型システム——をまさに解決します。518519Cloudflare Workers でのマイクロサービス、Node モノリスでの Express の置き換え、Vercel での関数、Bun での API に使うでしょう。同じ知識がどこへでも転用可能です——だからこそ今後数年の堅実な投資なのです。520521> **開始チェックリスト:**522>523> - [x] `npm create hono@latest` でランタイムテンプレートを選択524> - [x] チェーンでルートを定義(`.get(...).post(...)`)525> - [x] グローバルミドルウェアとして `logger`、`cors`、`secureHeaders` を追加526> - [x] `@hono/zod-validator` ですべての入力を検証527> - [x] `AppType` をエクスポートし、型安全な `hc` クライアントで API を消費528> - [x] HTTP サーバー不要の `app.request()` でテストを書く529> - [x] `wrangler deploy`(CF)、`vercel deploy`、ランタイムのバンドラでデプロイ530
:Hono:Web Standards 上に構築された超高速 Web フレームワークlines 1-530 (END) — press q to close