Hono完全実践ガイド〜Cloudflare Workers/Bun/Node・RPC・Validator・JSX【2026年版】〜

「Hono を Cloudflare Workers / Bun / Node / Deno で動かしたい」「RPC・Validator・JSX・JWT・WebSocket まで実コードで一気に押さえたい」。そんな声に応える完全実践ガイドです。本記事は Hono 4.x を前提に、最小サーバーから本番運用までを 40 個以上のコピペで動く TypeScript コードで解説します。Express との違い、マルチランタイム対応、エッジでの D1 / KV / R2 連携、zod-openapi まで網羅します。

この記事を最後まで読むと身につくこと

  • Hono 4 の最小サーバー〜本番運用までの全コード
  • Cloudflare Workers / Bun / Node / Deno / Vercel Edge / AWS Lambda 全ランタイム対応のデプロイ手順
  • Zod Validator + RPC によるサーバー/クライアント型共有
  • JWT / Cookie / CORS / Logger / Timing など必須 middleware
  • D1 / KV / R2 / Service Bindings によるエッジネイティブ実装
  • WebSocket / SSE / Streaming / JSX SSR + Suspense
  • zod-openapi で OpenAPI 仕様を自動生成、Express からの移行手順
  1. 1. Hono の概要とランタイム選定
    1. 1.1 Hono とは何か
    2. 1.2 Hono の3つの強み
    3. 1.3 Express / Fastify との比較
    4. 1.4 インストールとプロジェクト作成
    5. 1.5 既存プロジェクトに追加する場合
  2. 2. 基本サーバーとレスポンスの種類
    1. 2.1 最小の Hono アプリ
    2. 2.2 text / json / html / notFound
    3. 2.3 リダイレクト・body・raw Response
  3. 3. ルーティングとパラメータ
    1. 3.1 HTTP メソッドの定義
    2. 3.2 パスパラメータ
    3. 3.3 クエリパラメータ
    4. 3.4 ルートのグルーピング
    5. 3.5 basePath とチェーン記法
  4. 4. middleware を使いこなす
    1. 4.1 middleware の基本(use と next)
    2. 4.2 ビルトイン middleware(logger / timing / cors)
    3. 4.3 自作 middleware(認証ガード)
    4. 4.4 context に値を渡す(c.set / c.get)
  5. 5. JWT 認証と Cookie 操作
    1. 5.1 JWT ミドルウェアで保護
    2. 5.2 Cookie の読み書き(getCookie / setCookie)
    3. 5.3 セッション風の自前ガード
  6. 6. Zod Validator と型安全な入力
    1. 6.1 @hono/zod-validator の導入
    2. 6.2 JSON ボディのバリデーション
    3. 6.3 クエリ / パラメータのバリデーション
    4. 6.4 バリデーションエラーをカスタム整形
  7. 7. RPC でサーバー/クライアントの型を共有する
    1. 7.1 Hono RPC とは
    2. 7.2 サーバー側(チェーン記法が必須)
    3. 7.3 クライアント側(完全な型補完)
    4. 7.4 ヘッダー注入と fetch 差し替え
    5. 7.5 React Query との組み合わせ
  8. 8. JSX と SSR で View を描く
    1. 8.1 tsconfig の設定
    2. 8.2 最小の JSX レンダー
    3. 8.3 Streaming SSR と Suspense
    4. 8.4 useRequestContext で context を JSX から触る
  9. 9. ファイル配信・ストリーミング・WebSocket・SSE
    1. 9.1 静的ファイル(Cloudflare Workers)
    2. 9.2 静的ファイル(Bun / Node)
    3. 9.3 ストリーミングレスポンス
    4. 9.4 SSE(Server-Sent Events)
    5. 9.5 WebSocket(Cloudflare Workers + Durable Objects)
    6. 9.6 WebSocket(Bun)
  10. 10. ランタイム別デプロイ完全ガイド
    1. 10.1 Cloudflare Workers
    2. 10.2 Bun
    3. 10.3 Node.js(@hono/node-server)
    4. 10.4 Deno
    5. 10.5 Vercel Edge Functions
    6. 10.6 AWS Lambda
    7. 10.7 Fastly Compute@Edge / Lagon / Netlify
  11. 11. Cloudflare スタック連携(D1 / KV / R2 / Service Bindings)
    1. 11.1 環境バインディングの型付け
    2. 11.2 D1(SQLite on edge)で CRUD
    3. 11.3 KV(Key-Value)で計数 / キャッシュ
    4. 11.4 R2(オブジェクトストレージ)
    5. 11.5 Service Bindings(マイクロサービス間)
  12. 12. RegExp Router・テスト・OpenAPI・移行
    1. 12.1 RegExp Router で高速化
    2. 12.2 SmartRouter(自動切替)
    3. 12.3 テスト戦略(app.request で fetch ライク)
    4. 12.4 OpenAPI 連携(@hono/zod-openapi)
    5. 12.5 Swagger UI を表示
    6. 12.6 Express からの移行マッピング
    7. 12.7 移行サンプル(Express → Hono)
    8. 12.8 パフォーマンス比較の目安
  13. 13. まとめとプログラミング学習のロードマップ

1. Hono の概要とランタイム選定

1.1 Hono とは何か

Hono は 「速くて軽い」を旗印に作られた Web フレームワークで、Cloudflare Workers・Bun・Deno・Node.js・Vercel Edge・AWS Lambda・Fastly Compute@Edge など あらゆるランタイムで同じコードが動くのが最大の特徴です。コアは Web 標準の Request / Response に依存しており、依存ライブラリゼロ、TypeScript ファースト、Express ライクなシンプル API で書けます。

1.2 Hono の3つの強み

  1. マルチランタイム: 1 つのソースで Cloudflare Workers から AWS Lambda まで対応
  2. 型安全な RPC: サーバーで定義したルートの型をクライアントが import するだけで利用可能
  3. 圧倒的な速度: 独自の RegExp / Trie ルーターが Express の数倍速い

1.3 Express / Fastify との比較

項目 Hono 4 Express 5 Fastify 4
ランタイム マルチ(CF/Bun/Node/Deno/Lambda) Node 専用 Node 中心(部分的にBun)
依存数 0(ゼロ依存) 約30 約30
型安全 RPC 標準搭載 なし(別途 tRPC 等) なし
バンドルサイズ 約14KB 数百KB+ 数百KB+
標準 Request/Response Web 標準 独自(req/res) 独自

1.4 インストールとプロジェクト作成

# 公式 create-hono で対話的にテンプレートを生成
npm create hono@latest my-hono-app

# ランタイムを聞かれるので選ぶ:
# ❯ cloudflare-workers
#   bun
#   nodejs
#   deno
#   vercel
#   aws-lambda

cd my-hono-app
npm install
npm run dev

1.5 既存プロジェクトに追加する場合

# Cloudflare Workers
npm install hono
npm install -D wrangler @cloudflare/workers-types

# Bun
bun add hono

# Node.js(@hono/node-server が必要)
npm install hono @hono/node-server

# Deno(deno.json で npm: 指定)
# import { Hono } from 'npm:hono'

2. 基本サーバーとレスポンスの種類

2.1 最小の Hono アプリ

// src/index.ts
import { Hono } from 'hono'

const app = new Hono()

app.get('/', (c) => c.text('Hello Hono!'))

export default app

2.2 text / json / html / notFound

import { Hono } from 'hono'

const app = new Hono()

// テキスト
app.get('/text', (c) => c.text('plain text'))

// JSON(型推論される)
app.get('/json', (c) => c.json({ ok: true, ts: Date.now() }))

// HTML
app.get('/html', (c) => c.html('<h1>Hono</h1>'))

// ステータスコード指定
app.get('/created', (c) => c.json({ id: 1 }, 201))

// ヘッダー付与
app.get('/header', (c) => {
  c.header('X-Powered-By', 'Hono')
  return c.text('with header')
})

// 404 ハンドラ
app.notFound((c) => c.json({ error: 'not found' }, 404))

// エラーハンドラ
app.onError((err, c) => {
  console.error(err)
  return c.json({ error: err.message }, 500)
})

export default app

2.3 リダイレクト・body・raw Response

app.get('/redirect', (c) => c.redirect('/json', 302))

app.get('/body', (c) => c.body('raw body', 200, { 'Content-Type': 'text/plain' }))

// 生の Response を返す(Web 標準)
app.get('/raw', () => new Response('raw', { status: 200 }))

3. ルーティングとパラメータ

3.1 HTTP メソッドの定義

import { Hono } from 'hono'

const app = new Hono()

app.get('/posts', (c) => c.json({ list: [] }))
app.post('/posts', (c) => c.json({ created: true }, 201))
app.put('/posts/:id', (c) => c.json({ updated: c.req.param('id') }))
app.patch('/posts/:id', (c) => c.json({ patched: c.req.param('id') }))
app.delete('/posts/:id', (c) => c.json({ deleted: c.req.param('id') }))

// 全メソッド
app.all('/any', (c) => c.text(`method: ${c.req.method}`))

// 複数メソッドをまとめて
app.on(['GET', 'POST'], '/multi', (c) => c.text('GET or POST'))

export default app

3.2 パスパラメータ

// /users/123
app.get('/users/:id', (c) => {
  const id = c.req.param('id') // string
  return c.json({ id })
})

// 複数パラメータ
app.get('/users/:userId/posts/:postId', (c) => {
  const { userId, postId } = c.req.param()
  return c.json({ userId, postId })
})

// 正規表現で制約(数字のみ)
app.get('/orders/:id{[0-9]+}', (c) => {
  return c.json({ orderId: Number(c.req.param('id')) })
})

// オプショナル
app.get('/api/:version?', (c) => {
  return c.json({ version: c.req.param('version') ?? 'v1' })
})

// ワイルドカード(残り全部)
app.get('/files/*', (c) => {
  const path = c.req.path
  return c.text(`path: ${path}`)
})

3.3 クエリパラメータ

// /search?q=hono&page=2&tags=js&tags=ts
app.get('/search', (c) => {
  const q = c.req.query('q')              // 'hono'
  const page = c.req.query('page') ?? '1' // '2'
  const tags = c.req.queries('tags')      // ['js', 'ts']
  const all = c.req.query()               // 全クエリオブジェクト

  return c.json({ q, page, tags, all })
})

3.4 ルートのグルーピング

import { Hono } from 'hono'

const api = new Hono()
api.get('/users', (c) => c.json({ scope: 'api/users' }))
api.get('/posts', (c) => c.json({ scope: 'api/posts' }))

const admin = new Hono()
admin.get('/dashboard', (c) => c.json({ scope: 'admin' }))

const app = new Hono()
app.route('/api', api)        // /api/users, /api/posts
app.route('/admin', admin)    // /admin/dashboard

export default app

3.5 basePath とチェーン記法

// basePath で全体にプレフィックス
const v1 = new Hono().basePath('/v1')
v1.get('/health', (c) => c.json({ status: 'ok' }))

// チェーン記法(RPC のために強く推奨)
const routes = new Hono()
  .get('/users', (c) => c.json({ list: [] }))
  .post('/users', (c) => c.json({ created: true }, 201))
  .get('/users/:id', (c) => c.json({ id: c.req.param('id') }))

export type AppType = typeof routes

4. middleware を使いこなす

4.1 middleware の基本(use と next)

import { Hono } from 'hono'

const app = new Hono()

// 全ルートに適用
app.use('*', async (c, next) => {
  const start = Date.now()
  await next()
  const ms = Date.now() - start
  c.header('X-Response-Time', `${ms}ms`)
})

// 特定パスにのみ適用
app.use('/api/*', async (c, next) => {
  c.header('X-API-Version', '1.0.0')
  await next()
})

app.get('/', (c) => c.text('Hello'))
app.get('/api/ping', (c) => c.json({ pong: true }))

export default app

4.2 ビルトイン middleware(logger / timing / cors)

import { Hono } from 'hono'
import { logger } from 'hono/logger'
import { timing } from 'hono/timing'
import { cors } from 'hono/cors'
import { secureHeaders } from 'hono/secure-headers'
import { prettyJSON } from 'hono/pretty-json'
import { compress } from 'hono/compress'

const app = new Hono()

app.use('*', logger())                 // [GET] /xxx 200 12ms など
app.use('*', timing())                 // Server-Timing ヘッダ
app.use('*', secureHeaders())          // CSP / X-Frame-Options 等
app.use('*', prettyJSON())             // ?pretty で整形 JSON
app.use('*', compress())               // gzip/deflate(対応ランタイムのみ)
app.use('/api/*', cors({
  origin: ['https://example.com', 'http://localhost:3000'],
  allowMethods: ['GET', 'POST', 'PUT', 'DELETE', 'OPTIONS'],
  allowHeaders: ['Content-Type', 'Authorization'],
  credentials: true,
  maxAge: 600,
}))

app.get('/', (c) => c.json({ ok: true }))
export default app

4.3 自作 middleware(認証ガード)

import { Hono } from 'hono'
import type { MiddlewareHandler } from 'hono'

const requireApiKey: MiddlewareHandler = async (c, next) => {
  const key = c.req.header('X-API-Key')
  if (key !== process.env.API_KEY) {
    return c.json({ error: 'unauthorized' }, 401)
  }
  await next()
}

const app = new Hono()
app.use('/api/*', requireApiKey)
app.get('/api/secret', (c) => c.json({ secret: 42 }))
export default app

4.4 context に値を渡す(c.set / c.get)

type Variables = { userId: string }
const app = new Hono<{ Variables: Variables }>()

app.use('*', async (c, next) => {
  c.set('userId', 'u_123')
  await next()
})

app.get('/me', (c) => {
  const userId = c.get('userId') // 型推論される
  return c.json({ userId })
})
export default app

5. JWT 認証と Cookie 操作

5.1 JWT ミドルウェアで保護

import { Hono } from 'hono'
import { jwt, sign } from 'hono/jwt'

const app = new Hono()
const SECRET = process.env.JWT_SECRET ?? 'dev-secret'

// ログインしてトークン発行
app.post('/login', async (c) => {
  const { email } = await c.req.json<{ email: string }>()
  const payload = {
    sub: email,
    exp: Math.floor(Date.now() / 1000) + 60 * 60, // 1h
  }
  const token = await sign(payload, SECRET)
  return c.json({ token })
})

// /api/* は JWT 必須
app.use('/api/*', jwt({ secret: SECRET }))

// payload は c.get('jwtPayload') で取れる
app.get('/api/me', (c) => {
  const payload = c.get('jwtPayload')
  return c.json({ payload })
})

export default app

5.2 Cookie の読み書き(getCookie / setCookie)

import { Hono } from 'hono'
import { getCookie, getSignedCookie, setCookie, setSignedCookie, deleteCookie } from 'hono/cookie'

const app = new Hono()
const COOKIE_SECRET = 'cookie-secret-32-bytes-min'

app.get('/cookie/set', (c) => {
  setCookie(c, 'session', 'abc123', {
    path: '/',
    httpOnly: true,
    secure: true,
    sameSite: 'Lax',
    maxAge: 60 * 60 * 24, // 1day
  })
  return c.text('ok')
})

app.get('/cookie/get', (c) => {
  const v = getCookie(c, 'session')
  return c.json({ session: v })
})

app.get('/cookie/signed/set', async (c) => {
  await setSignedCookie(c, 'uid', 'u_42', COOKIE_SECRET, { httpOnly: true })
  return c.text('ok')
})

app.get('/cookie/signed/get', async (c) => {
  const v = await getSignedCookie(c, COOKIE_SECRET, 'uid')
  return c.json({ uid: v })
})

app.get('/cookie/clear', (c) => {
  deleteCookie(c, 'session', { path: '/' })
  return c.text('cleared')
})

export default app

5.3 セッション風の自前ガード

import { Hono } from 'hono'
import { getSignedCookie } from 'hono/cookie'

const SECRET = process.env.COOKIE_SECRET!

const app = new Hono<{ Variables: { userId: string } }>()

app.use('/dashboard/*', async (c, next) => {
  const uid = await getSignedCookie(c, SECRET, 'uid')
  if (!uid) return c.redirect('/login', 302)
  c.set('userId', uid)
  await next()
})

app.get('/dashboard', (c) => c.text(`hello ${c.get('userId')}`))
export default app

6. Zod Validator と型安全な入力

6.1 @hono/zod-validator の導入

npm install zod @hono/zod-validator

6.2 JSON ボディのバリデーション

import { Hono } from 'hono'
import { z } from 'zod'
import { zValidator } from '@hono/zod-validator'

const schema = z.object({
  title: z.string().min(1).max(120),
  body: z.string().min(1),
  tags: z.array(z.string()).max(10).optional(),
})

const app = new Hono()

app.post('/posts', zValidator('json', schema), (c) => {
  const data = c.req.valid('json') // ←型推論される
  // data.title, data.body, data.tags
  return c.json({ created: true, data }, 201)
})

export default app

6.3 クエリ / パラメータのバリデーション

import { Hono } from 'hono'
import { z } from 'zod'
import { zValidator } from '@hono/zod-validator'

const querySchema = z.object({
  page: z.coerce.number().int().min(1).default(1),
  per: z.coerce.number().int().min(1).max(100).default(20),
  q: z.string().optional(),
})

const paramSchema = z.object({
  id: z.coerce.number().int().positive(),
})

const app = new Hono()

app.get(
  '/posts',
  zValidator('query', querySchema),
  (c) => {
    const { page, per, q } = c.req.valid('query')
    return c.json({ page, per, q, items: [] })
  },
)

app.get(
  '/posts/:id',
  zValidator('param', paramSchema),
  (c) => {
    const { id } = c.req.valid('param')
    return c.json({ id })
  },
)

export default app

6.4 バリデーションエラーをカスタム整形

app.post(
  '/posts',
  zValidator('json', schema, (result, c) => {
    if (!result.success) {
      return c.json({
        ok: false,
        errors: result.error.flatten().fieldErrors,
      }, 400)
    }
  }),
  (c) => c.json({ created: true }, 201),
)

7. RPC でサーバー/クライアントの型を共有する

7.1 Hono RPC とは

Hono の最大の武器が RPC モードです。サーバーで appexport type するだけで、クライアントは hono/client 経由でパス・メソッド・入力型・レスポンス型すべてを補完しながら呼び出せます。tRPC のようなコード生成や OpenAPI 経由のクライアント自動生成は不要です。

7.2 サーバー側(チェーン記法が必須)

// server.ts
import { Hono } from 'hono'
import { z } from 'zod'
import { zValidator } from '@hono/zod-validator'

const route = new Hono()
  .get('/users', (c) => c.json([{ id: 1, name: 'taro' }]))
  .post(
    '/users',
    zValidator('json', z.object({ name: z.string().min(1) })),
    (c) => {
      const { name } = c.req.valid('json')
      return c.json({ id: 2, name }, 201)
    },
  )
  .get('/users/:id', (c) => c.json({ id: Number(c.req.param('id')) }))

const app = new Hono().route('/api', route)

export type AppType = typeof app
export default app

7.3 クライアント側(完全な型補完)

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

const client = hc<AppType>('http://localhost:8787')

// GET /api/users
const res = await client.api.users.$get()
const users = await res.json() // (User[]) 型推論

// POST /api/users
const created = await client.api.users.$post({
  json: { name: 'jiro' }, // ←Zod スキーマから推論
})
console.log(await created.json())

// GET /api/users/:id
const one = await client.api.users[':id'].$get({
  param: { id: '1' },
})
console.log(await one.json())

7.4 ヘッダー注入と fetch 差し替え

const client = hc<AppType>('http://localhost:8787', {
  headers: () => ({ Authorization: `Bearer ${getToken()}` }),
  fetch: (input, init) => fetch(input, { ...init, credentials: 'include' }),
})

7.5 React Query との組み合わせ

import { useQuery } from '@tanstack/react-query'
import { hc, InferResponseType } from 'hono/client'
import type { AppType } from '../server'

const client = hc<AppType>('/')
type Users = InferResponseType<typeof client.api.users.$get>

export function useUsers() {
  return useQuery<Users>({
    queryKey: ['users'],
    queryFn: async () => {
      const res = await client.api.users.$get()
      if (!res.ok) throw new Error('failed')
      return res.json()
    },
  })
}

8. JSX と SSR で View を描く

8.1 tsconfig の設定

// tsconfig.json
{
  "compilerOptions": {
    "jsx": "react-jsx",
    "jsxImportSource": "hono/jsx"
  }
}

8.2 最小の JSX レンダー

// src/index.tsx
import { Hono } from 'hono'

const app = new Hono()

const Layout = (props: { title: string; children?: any }) => (
  <html>
    <head><title>{props.title}</title></head>
    <body>{props.children}</body>
  </html>
)

app.get('/', (c) => {
  return c.html(
    <Layout title="Top">
      <h1>Hello Hono JSX</h1>
      <p>rendered on server</p>
    </Layout>
  )
})

export default app

8.3 Streaming SSR と Suspense

import { Hono } from 'hono'
import { Suspense } from 'hono/jsx'
import { renderToReadableStream } from 'hono/jsx/streaming'

const Slow = async () => {
  await new Promise((r) => setTimeout(r, 500))
  return <p>loaded after 500ms</p>
}

const app = new Hono()

app.get('/stream', (c) => {
  const stream = renderToReadableStream(
    <html>
      <body>
        <h1>Streaming</h1>
        <Suspense fallback={<p>loading...</p>}>
          <Slow />
        </Suspense>
      </body>
    </html>,
  )
  return c.body(stream, 200, { 'Content-Type': 'text/html; charset=utf-8' })
})

export default app

8.4 useRequestContext で context を JSX から触る

import { useRequestContext } from 'hono/jsx-renderer'
import { jsxRenderer } from 'hono/jsx-renderer'

const app = new Hono()

app.get('*', jsxRenderer(({ children }) => (
  <html><body>{children}</body></html>
)))

const User = () => {
  const c = useRequestContext()
  const id = c.req.query('id') ?? 'guest'
  return <p>user: {id}</p>
}

app.get('/', (c) => c.render(<User />))
export default app

9. ファイル配信・ストリーミング・WebSocket・SSE

9.1 静的ファイル(Cloudflare Workers)

// wrangler.toml で [site] バインドした場合
import { Hono } from 'hono'
import { serveStatic } from 'hono/cloudflare-workers'

const app = new Hono()
app.get('/static/*', serveStatic({ root: './' }))
app.get('/', (c) => c.text('home'))
export default app

9.2 静的ファイル(Bun / Node)

// Bun
import { Hono } from 'hono'
import { serveStatic } from 'hono/bun'
const app = new Hono()
app.use('/public/*', serveStatic({ root: './' }))

// Node.js
// import { serveStatic } from '@hono/node-server/serve-static'
// app.use('/public/*', serveStatic({ root: './public' }))

9.3 ストリーミングレスポンス

import { Hono } from 'hono'
import { stream, streamText } from 'hono/streaming'

const app = new Hono()

app.get('/stream/text', (c) => {
  return streamText(c, async (s) => {
    for (let i = 0; i < 5; i++) {
      await s.write(`line ${i}n`)
      await s.sleep(200)
    }
  })
})

app.get('/stream/binary', (c) => {
  return stream(c, async (s) => {
    await s.write(new Uint8Array([1, 2, 3]))
    await s.write(new Uint8Array([4, 5, 6]))
  })
})

export default app

9.4 SSE(Server-Sent Events)

import { Hono } from 'hono'
import { streamSSE } from 'hono/streaming'

const app = new Hono()

app.get('/sse', (c) => {
  return streamSSE(c, async (s) => {
    let id = 0
    while (true) {
      await s.writeSSE({
        event: 'tick',
        data: JSON.stringify({ now: Date.now() }),
        id: String(id++),
      })
      await s.sleep(1000)
      if (id > 30) break
    }
  })
})

export default app

9.5 WebSocket(Cloudflare Workers + Durable Objects)

import { Hono } from 'hono'
import { upgradeWebSocket } from 'hono/cloudflare-workers'

const app = new Hono()

app.get(
  '/ws',
  upgradeWebSocket((c) => ({
    onOpen: () => console.log('opened'),
    onMessage: (event, ws) => {
      ws.send(`echo: ${event.data}`)
    },
    onClose: () => console.log('closed'),
  })),
)

export default app

9.6 WebSocket(Bun)

import { Hono } from 'hono'
import { createBunWebSocket } from 'hono/bun'

const { upgradeWebSocket, websocket } = createBunWebSocket()
const app = new Hono()

app.get('/chat', upgradeWebSocket(() => ({
  onMessage(event, ws) {
    ws.send(`server: ${event.data}`)
  },
})))

export default { fetch: app.fetch, websocket }

10. ランタイム別デプロイ完全ガイド

10.1 Cloudflare Workers

# wrangler.toml
name = "my-hono-app"
main = "src/index.ts"
compatibility_date = "2026-04-01"
compatibility_flags = ["nodejs_compat"]

# package.json scripts
# "dev": "wrangler dev"
# "deploy": "wrangler deploy"

npx wrangler dev
npx wrangler deploy

10.2 Bun

// src/index.ts
import { Hono } from 'hono'
const app = new Hono()
app.get('/', (c) => c.text('Hello from Bun'))

export default {
  port: 3000,
  fetch: app.fetch,
}

# 起動
bun run --hot src/index.ts

10.3 Node.js(@hono/node-server)

import { serve } from '@hono/node-server'
import { Hono } from 'hono'

const app = new Hono()
app.get('/', (c) => c.text('Hello from Node'))

serve({
  fetch: app.fetch,
  port: 3000,
}, (info) => {
  console.log(`Listening on http://localhost:${info.port}`)
})

10.4 Deno

// main.ts
import { Hono } from 'npm:hono'
const app = new Hono()
app.get('/', (c) => c.text('Hello from Deno'))
Deno.serve(app.fetch)

# 実行
deno run --allow-net main.ts

10.5 Vercel Edge Functions

// api/[[...route]].ts
import { Hono } from 'hono'
import { handle } from 'hono/vercel'

export const config = { runtime: 'edge' }

const app = new Hono().basePath('/api')
app.get('/hello', (c) => c.json({ msg: 'hi from vercel edge' }))

export default handle(app)

10.6 AWS Lambda

// lambda.ts
import { Hono } from 'hono'
import { handle } from 'hono/aws-lambda'

const app = new Hono()
app.get('/', (c) => c.text('Hello from Lambda'))

export const handler = handle(app)

10.7 Fastly Compute@Edge / Lagon / Netlify

// Netlify Functions
import { Hono } from 'hono'
import { handle } from 'hono/netlify'

const app = new Hono().basePath('/.netlify/functions/api')
app.get('/hello', (c) => c.json({ ok: true }))

export default handle(app)

11. Cloudflare スタック連携(D1 / KV / R2 / Service Bindings)

11.1 環境バインディングの型付け

// src/types.d.ts
type Bindings = {
  DB: D1Database
  KV: KVNamespace
  R2: R2Bucket
  AUTH: Fetcher        // Service Binding
  API_KEY: string
}

// src/index.ts
import { Hono } from 'hono'
const app = new Hono<{ Bindings: Bindings }>()
app.get('/env', (c) => c.json({ key: c.env.API_KEY }))
export default app

11.2 D1(SQLite on edge)で CRUD

const app = new Hono<{ Bindings: Bindings }>()

app.get('/posts', async (c) => {
  const { results } = await c.env.DB
    .prepare('SELECT id, title FROM posts ORDER BY id DESC LIMIT 50')
    .all()
  return c.json(results)
})

app.post('/posts', async (c) => {
  const { title, body } = await c.req.json<{ title: string; body: string }>()
  const { meta } = await c.env.DB
    .prepare('INSERT INTO posts(title, body) VALUES(?, ?)')
    .bind(title, body)
    .run()
  return c.json({ id: meta.last_row_id }, 201)
})

app.get('/posts/:id', async (c) => {
  const id = Number(c.req.param('id'))
  const row = await c.env.DB
    .prepare('SELECT * FROM posts WHERE id = ?')
    .bind(id)
    .first()
  if (!row) return c.json({ error: 'not found' }, 404)
  return c.json(row)
})

export default app

11.3 KV(Key-Value)で計数 / キャッシュ

app.get('/count', async (c) => {
  const cur = Number(await c.env.KV.get('count') ?? '0')
  const next = cur + 1
  await c.env.KV.put('count', String(next), { expirationTtl: 60 * 60 })
  return c.json({ count: next })
})

app.get('/cache/:k', async (c) => {
  const k = c.req.param('k')
  const cached = await c.env.KV.get(k, 'json')
  if (cached) return c.json({ from: 'cache', data: cached })
  const data = { ts: Date.now() }
  await c.env.KV.put(k, JSON.stringify(data), { expirationTtl: 30 })
  return c.json({ from: 'origin', data })
})

11.4 R2(オブジェクトストレージ)

// アップロード
app.put('/r2/:key', async (c) => {
  const key = c.req.param('key')
  const body = await c.req.arrayBuffer()
  await c.env.R2.put(key, body, {
    httpMetadata: { contentType: c.req.header('Content-Type') ?? 'application/octet-stream' },
  })
  return c.json({ key, size: body.byteLength }, 201)
})

// ダウンロード
app.get('/r2/:key', async (c) => {
  const obj = await c.env.R2.get(c.req.param('key'))
  if (!obj) return c.notFound()
  const headers = new Headers()
  obj.writeHttpMetadata(headers)
  headers.set('etag', obj.httpEtag)
  return new Response(obj.body, { headers })
})

11.5 Service Bindings(マイクロサービス間)

// wrangler.toml(呼び出し側)
# [[services]]
# binding = "AUTH"
# service = "auth-service"

app.get('/whoami', async (c) => {
  const res = await c.env.AUTH.fetch(new Request('https://auth/me', {
    headers: { Authorization: c.req.header('Authorization') ?? '' },
  }))
  return new Response(res.body, res)
})

12. RegExp Router・テスト・OpenAPI・移行

12.1 RegExp Router で高速化

import { Hono } from 'hono'
import { RegExpRouter } from 'hono/router/reg-exp-router'

const app = new Hono({ router: new RegExpRouter() })
app.get('/', (c) => c.text('fastest router'))
export default app

12.2 SmartRouter(自動切替)

import { Hono } from 'hono'
import { SmartRouter } from 'hono/router/smart-router'
import { RegExpRouter } from 'hono/router/reg-exp-router'
import { TrieRouter } from 'hono/router/trie-router'

const app = new Hono({
  router: new SmartRouter({ routers: [new RegExpRouter(), new TrieRouter()] }),
})

12.3 テスト戦略(app.request で fetch ライク)

// vitest
import { describe, it, expect } from 'vitest'
import app from '../src/index'

describe('GET /', () => {
  it('returns 200', async () => {
    const res = await app.request('/')
    expect(res.status).toBe(200)
    expect(await res.text()).toBe('Hello Hono!')
  })

  it('POST /posts validates body', async () => {
    const res = await app.request('/posts', {
      method: 'POST',
      headers: { 'Content-Type': 'application/json' },
      body: JSON.stringify({ title: 't', body: 'b' }),
    })
    expect(res.status).toBe(201)
  })
})

12.4 OpenAPI 連携(@hono/zod-openapi)

import { OpenAPIHono, createRoute, z } from '@hono/zod-openapi'

const app = new OpenAPIHono()

const route = createRoute({
  method: 'get',
  path: '/users/{id}',
  request: { params: z.object({ id: z.string() }) },
  responses: {
    200: {
      content: {
        'application/json': {
          schema: z.object({ id: z.string(), name: z.string() }),
        },
      },
      description: 'User',
    },
  },
})

app.openapi(route, (c) => {
  const { id } = c.req.valid('param')
  return c.json({ id, name: 'taro' })
})

// OpenAPI ドキュメント JSON
app.doc('/openapi.json', {
  openapi: '3.0.0',
  info: { title: 'My API', version: '1.0.0' },
})

export default app

12.5 Swagger UI を表示

import { swaggerUI } from '@hono/swagger-ui'
app.get('/docs', swaggerUI({ url: '/openapi.json' }))

12.6 Express からの移行マッピング

Express Hono
req.params.id c.req.param('id')
req.query.q c.req.query('q')
req.body (express.json) await c.req.json()
res.json(x) c.json(x)
res.status(201).json(x) c.json(x, 201)
res.redirect('/x') c.redirect('/x')
app.use(cors()) app.use('*', cors())
app.listen(3000) ランタイム別(serve() 等)

12.7 移行サンプル(Express → Hono)

// Before: Express
import express from 'express'
const app = express()
app.use(express.json())
app.get('/users/:id', (req, res) => {
  res.status(200).json({ id: req.params.id })
})
app.listen(3000)

// After: Hono(Node でも CF でも動く)
import { Hono } from 'hono'
import { serve } from '@hono/node-server'
const app = new Hono()
app.get('/users/:id', (c) => c.json({ id: c.req.param('id') }))
serve({ fetch: app.fetch, port: 3000 })

12.8 パフォーマンス比較の目安

フレームワーク req/s 目安 特徴
Hono(Bun) 非常に高い Web 標準 + ゼロ依存 + Bun の最適化
Fastify(Node) 高い JSON スキーマ最適化、Node 専用
Express(Node) 枯れた資産が豊富、middleware エコシステム

※ 実測値はランタイム / マシン / ベンチ条件で大きく変わります。重要なのは「Hono は Web 標準 Request/Response 上で動くので、エッジ環境でのコールドスタートに圧倒的に強い」という点です。

13. まとめとプログラミング学習のロードマップ

Hono は マルチランタイム × 型安全 RPC × 軽量という従来の Node 系フレームワークにはない組み合わせを実現しています。本記事のコードはそのまま npm create hono 直後のプロジェクトに貼り付けて動かせるので、まずは Cloudflare Workers + Wrangler で wrangler devwrangler deploy までを一気に通してみてください。Express からの移行も、ルートハンドラの中身はほぼ 1 対 1 で書き換えられます。

こうした エッジネイティブな TypeScript フルスタックは、もはやモダンバックエンドの標準スキルになりつつあります。独学で詰まりやすいのは「Cloudflare の各サービス(D1/KV/R2/Queues/Durable Objects)を組み合わせた現場アーキテクチャ」「OpenAPI ベースの API 設計」「監視・運用」の領域で、ここはメンター付きのスクールで一気に習得すると最短距離です。

Hono / モダンバックエンドを学べるおすすめスクール

  • テックアカデミー:Node.js / TypeScript / クラウド系コースが豊富。現役エンジニアのマンツーマンメンタリングで Hono のような新興フレームワークも質問可能
  • 侍エンジニア:完全オーダーメイドカリキュラム。「Hono + Cloudflare Workers で SaaS を作りたい」など具体的な目標に合わせて学習計画を組める
  • DMM WEBCAMP:バックエンドからクラウドまで体系的に。実務に近いチーム開発カリキュラムあり
  • レバテックカレッジ:大学生向け短期集中。TypeScript / API 開発の基礎を最短で習得し、長期インターンへ橋渡し

独学では到達しづらい「API 設計」「クラウドアーキテクチャ」「運用」までを最短で身につけるなら、まずは無料カウンセリングで 自分のキャリア目標(Web 開発 / クラウドエンジニア / 副業 / フリーランス) を相談してみてください。Hono のような最新フレームワークをいち早くキャッチアップできる土台が、確実に短期間で手に入ります。

コメント

タイトルとURLをコピーしました