「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. Hono の概要とランタイム選定
- 2. 基本サーバーとレスポンスの種類
- 3. ルーティングとパラメータ
- 4. middleware を使いこなす
- 5. JWT 認証と Cookie 操作
- 6. Zod Validator と型安全な入力
- 7. RPC でサーバー/クライアントの型を共有する
- 8. JSX と SSR で View を描く
- 9. ファイル配信・ストリーミング・WebSocket・SSE
- 10. ランタイム別デプロイ完全ガイド
- 11. Cloudflare スタック連携(D1 / KV / R2 / Service Bindings)
- 12. RegExp Router・テスト・OpenAPI・移行
- 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 つのソースで Cloudflare Workers から AWS Lambda まで対応
- 型安全な RPC: サーバーで定義したルートの型をクライアントが import するだけで利用可能
- 圧倒的な速度: 独自の 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 モードです。サーバーで app を export 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 dev → wrangler deploy までを一気に通してみてください。Express からの移行も、ルートハンドラの中身はほぼ 1 対 1 で書き換えられます。
こうした エッジネイティブな TypeScript フルスタックは、もはやモダンバックエンドの標準スキルになりつつあります。独学で詰まりやすいのは「Cloudflare の各サービス(D1/KV/R2/Queues/Durable Objects)を組み合わせた現場アーキテクチャ」「OpenAPI ベースの API 設計」「監視・運用」の領域で、ここはメンター付きのスクールで一気に習得すると最短距離です。
- テックアカデミー:Node.js / TypeScript / クラウド系コースが豊富。現役エンジニアのマンツーマンメンタリングで Hono のような新興フレームワークも質問可能
- 侍エンジニア:完全オーダーメイドカリキュラム。「Hono + Cloudflare Workers で SaaS を作りたい」など具体的な目標に合わせて学習計画を組める
- DMM WEBCAMP:バックエンドからクラウドまで体系的に。実務に近いチーム開発カリキュラムあり
- レバテックカレッジ:大学生向け短期集中。TypeScript / API 開発の基礎を最短で習得し、長期インターンへ橋渡し
独学では到達しづらい「API 設計」「クラウドアーキテクチャ」「運用」までを最短で身につけるなら、まずは無料カウンセリングで 自分のキャリア目標(Web 開発 / クラウドエンジニア / 副業 / フリーランス) を相談してみてください。Hono のような最新フレームワークをいち早くキャッチアップできる土台が、確実に短期間で手に入ります。

コメント