Hono + Cloudflare Workers実践ガイド — エッジで動く超高速API


Hono + Cloudflare Workersとは

Honoは超軽量・超高速なTypeScript Webフレームワークで、Cloudflare Workersでのパフォーマンスは圧倒的です。

なぜHono + Cloudflare Workersなのか

  • 超高速: エッジで処理、レイテンシ10ms以下
  • スケーラブル: 自動スケーリング、設定不要
  • 低コスト: 無料枠で月間10万リクエスト
  • 型安全: TypeScriptファーストで完全な型推論
  • グローバル: 世界200拠点以上で配信

2026年現在、エッジコンピューティングの最有力スタックの一つです。

プロジェクトセットアップ

新規プロジェクト作成

npm create hono@latest my-edge-app
cd my-edge-app

# テンプレート選択で "cloudflare-workers" を選ぶ

手動セットアップ

mkdir my-edge-app
cd my-edge-app
npm init -y
npm install hono
npm install -D wrangler

wrangler.tomlを作成:

name = "my-edge-app"
main = "src/index.ts"
compatibility_date = "2026-02-05"

[observability]
enabled = true

TypeScript設定

tsconfig.json:

{
  "compilerOptions": {
    "target": "ES2021",
    "module": "ESNext",
    "lib": ["ES2021"],
    "moduleResolution": "bundler",
    "types": ["@cloudflare/workers-types"],
    "jsx": "react-jsx",
    "jsxImportSource": "hono/jsx",
    "strict": true,
    "skipLibCheck": true
  }
}

インストール:

npm install -D @cloudflare/workers-types

基本的なAPI作成

Hello World

src/index.ts:

import { Hono } from 'hono'

const app = new Hono()

app.get('/', (c) => {
  return c.json({
    message: 'Hello from Cloudflare Workers!',
    timestamp: new Date().toISOString(),
    location: c.req.header('cf-ray')
  })
})

export default app

ローカル開発

npm run dev
# または
npx wrangler dev

http://localhost:8787 でアクセス可能。

デプロイ

npx wrangler deploy

デプロイ後、https://my-edge-app.workers.devでアクセスできます。

Cloudflare D1(SQLite)連携

D1データベース作成

npx wrangler d1 create my-database

wrangler.tomlに追加:

[[d1_databases]]
binding = "DB"
database_name = "my-database"
database_id = "xxx-xxx-xxx"  # 出力されたID

テーブル作成

schema.sql:

CREATE TABLE users (
  id INTEGER PRIMARY KEY AUTOINCREMENT,
  name TEXT NOT NULL,
  email TEXT UNIQUE NOT NULL,
  created_at DATETIME DEFAULT CURRENT_TIMESTAMP
);

CREATE TABLE posts (
  id INTEGER PRIMARY KEY AUTOINCREMENT,
  user_id INTEGER NOT NULL,
  title TEXT NOT NULL,
  content TEXT,
  created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
  FOREIGN KEY (user_id) REFERENCES users(id)
);

CREATE INDEX idx_posts_user_id ON posts(user_id);

実行:

npx wrangler d1 execute my-database --file=./schema.sql

Hono + D1実装

型定義:

import { Hono } from 'hono'

type Bindings = {
  DB: D1Database
}

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

CRUD操作:

// ユーザー一覧取得
app.get('/api/users', async (c) => {
  const { results } = await c.env.DB.prepare(
    'SELECT id, name, email, created_at FROM users ORDER BY created_at DESC'
  ).all()

  return c.json(results)
})

// ユーザー詳細取得
app.get('/api/users/:id', async (c) => {
  const id = c.req.param('id')

  const user = await c.env.DB.prepare(
    'SELECT * FROM users WHERE id = ?'
  ).bind(id).first()

  if (!user) {
    return c.json({ error: 'User not found' }, 404)
  }

  return c.json(user)
})

// ユーザー作成
app.post('/api/users', async (c) => {
  const { name, email } = await c.req.json()

  try {
    const result = await c.env.DB.prepare(
      'INSERT INTO users (name, email) VALUES (?, ?)'
    ).bind(name, email).run()

    return c.json({
      id: result.meta.last_row_id,
      name,
      email
    }, 201)
  } catch (error) {
    return c.json({ error: 'Email already exists' }, 400)
  }
})

// ユーザー更新
app.put('/api/users/:id', async (c) => {
  const id = c.req.param('id')
  const { name, email } = await c.req.json()

  const result = await c.env.DB.prepare(
    'UPDATE users SET name = ?, email = ? WHERE id = ?'
  ).bind(name, email, id).run()

  if (result.meta.changes === 0) {
    return c.json({ error: 'User not found' }, 404)
  }

  return c.json({ success: true })
})

// ユーザー削除
app.delete('/api/users/:id', async (c) => {
  const id = c.req.param('id')

  const result = await c.env.DB.prepare(
    'DELETE FROM users WHERE id = ?'
  ).bind(id).run()

  if (result.meta.changes === 0) {
    return c.json({ error: 'User not found' }, 404)
  }

  return c.json({ success: true })
})

トランザクション

app.post('/api/posts', async (c) => {
  const { user_id, title, content } = await c.req.json()

  // トランザクション
  const results = await c.env.DB.batch([
    c.env.DB.prepare('SELECT id FROM users WHERE id = ?').bind(user_id),
    c.env.DB.prepare(
      'INSERT INTO posts (user_id, title, content) VALUES (?, ?, ?)'
    ).bind(user_id, title, content)
  ])

  if (!results[0].results.length) {
    return c.json({ error: 'User not found' }, 404)
  }

  return c.json({
    id: results[1].meta.last_row_id,
    title,
    content
  }, 201)
})

JOIN クエリ

app.get('/api/posts', async (c) => {
  const { results } = await c.env.DB.prepare(`
    SELECT
      posts.id,
      posts.title,
      posts.content,
      posts.created_at,
      users.name as author_name,
      users.email as author_email
    FROM posts
    JOIN users ON posts.user_id = users.id
    ORDER BY posts.created_at DESC
  `).all()

  return c.json(results)
})

Cloudflare KV(Key-Value Store)連携

KV Namespace作成

npx wrangler kv:namespace create CACHE
npx wrangler kv:namespace create CACHE --preview

wrangler.tomlに追加:

[[kv_namespaces]]
binding = "CACHE"
id = "xxx"
preview_id = "yyy"

Hono + KV実装

型定義:

type Bindings = {
  DB: D1Database
  CACHE: KVNamespace
}

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

基本操作:

// キャッシュ取得
app.get('/api/cache/:key', async (c) => {
  const key = c.req.param('key')
  const value = await c.env.CACHE.get(key)

  if (!value) {
    return c.json({ error: 'Not found' }, 404)
  }

  return c.json({ key, value })
})

// キャッシュ保存
app.post('/api/cache', async (c) => {
  const { key, value, ttl } = await c.req.json()

  await c.env.CACHE.put(key, value, {
    expirationTtl: ttl || 3600  // 1時間
  })

  return c.json({ success: true })
})

// キャッシュ削除
app.delete('/api/cache/:key', async (c) => {
  const key = c.req.param('key')
  await c.env.CACHE.delete(key)

  return c.json({ success: true })
})

キャッシュパターン

Cache-Aside:

app.get('/api/users/:id', async (c) => {
  const id = c.req.param('id')
  const cacheKey = `user:${id}`

  // キャッシュチェック
  const cached = await c.env.CACHE.get(cacheKey, 'json')
  if (cached) {
    return c.json({ ...cached, cached: true })
  }

  // DB から取得
  const user = await c.env.DB.prepare(
    'SELECT * FROM users WHERE id = ?'
  ).bind(id).first()

  if (!user) {
    return c.json({ error: 'Not found' }, 404)
  }

  // キャッシュに保存(1時間)
  await c.env.CACHE.put(cacheKey, JSON.stringify(user), {
    expirationTtl: 3600
  })

  return c.json({ ...user, cached: false })
})

Write-Through:

app.put('/api/users/:id', async (c) => {
  const id = c.req.param('id')
  const { name, email } = await c.req.json()

  // DB更新
  const result = await c.env.DB.prepare(
    'UPDATE users SET name = ?, email = ? WHERE id = ?'
  ).bind(name, email, id).run()

  if (result.meta.changes === 0) {
    return c.json({ error: 'Not found' }, 404)
  }

  // キャッシュ削除(または更新)
  await c.env.CACHE.delete(`user:${id}`)

  return c.json({ success: true })
})

セッション管理

import { v4 as uuidv4 } from 'uuid'

app.post('/api/sessions', async (c) => {
  const { user_id } = await c.req.json()

  const sessionId = uuidv4()
  const sessionData = {
    user_id,
    created_at: new Date().toISOString()
  }

  // セッション保存(24時間)
  await c.env.CACHE.put(
    `session:${sessionId}`,
    JSON.stringify(sessionData),
    { expirationTtl: 86400 }
  )

  return c.json({ session_id: sessionId })
})

// セッション検証ミドルウェア
app.use('/api/protected/*', async (c, next) => {
  const sessionId = c.req.header('x-session-id')

  if (!sessionId) {
    return c.json({ error: 'Unauthorized' }, 401)
  }

  const session = await c.env.CACHE.get(`session:${sessionId}`, 'json')

  if (!session) {
    return c.json({ error: 'Session expired' }, 401)
  }

  c.set('session', session)
  await next()
})

Cloudflare R2(Object Storage)連携

R2 Bucket作成

npx wrangler r2 bucket create my-bucket

wrangler.toml:

[[r2_buckets]]
binding = "BUCKET"
bucket_name = "my-bucket"

Hono + R2実装

型定義:

type Bindings = {
  DB: D1Database
  CACHE: KVNamespace
  BUCKET: R2Bucket
}

ファイルアップロード:

app.post('/api/upload', async (c) => {
  const formData = await c.req.formData()
  const file = formData.get('file') as File

  if (!file) {
    return c.json({ error: 'No file provided' }, 400)
  }

  const key = `uploads/${Date.now()}-${file.name}`

  await c.env.BUCKET.put(key, file.stream(), {
    httpMetadata: {
      contentType: file.type
    }
  })

  return c.json({
    key,
    url: `https://pub-xxx.r2.dev/${key}`
  })
})

ファイル取得:

app.get('/api/files/:key', async (c) => {
  const key = c.req.param('key')

  const object = await c.env.BUCKET.get(key)

  if (!object) {
    return c.json({ error: 'Not found' }, 404)
  }

  return new Response(object.body, {
    headers: {
      'Content-Type': object.httpMetadata?.contentType || 'application/octet-stream',
      'Cache-Control': 'public, max-age=3600'
    }
  })
})

ファイル一覧:

app.get('/api/files', async (c) => {
  const prefix = c.req.query('prefix') || ''
  const limit = Number(c.req.query('limit') || '100')

  const list = await c.env.BUCKET.list({ prefix, limit })

  return c.json({
    objects: list.objects.map(obj => ({
      key: obj.key,
      size: obj.size,
      uploaded: obj.uploaded
    })),
    truncated: list.truncated
  })
})

ファイル削除:

app.delete('/api/files/:key', async (c) => {
  const key = c.req.param('key')

  await c.env.BUCKET.delete(key)

  return c.json({ success: true })
})

認証実装

JWT認証

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

const JWT_SECRET = 'your-secret-key'

// ログイン
app.post('/api/login', async (c) => {
  const { email, password } = await c.req.json()

  const user = await c.env.DB.prepare(
    'SELECT * FROM users WHERE email = ?'
  ).bind(email).first()

  if (!user || user.password !== password) {
    return c.json({ error: 'Invalid credentials' }, 401)
  }

  const token = await sign(
    {
      sub: user.id,
      email: user.email,
      exp: Math.floor(Date.now() / 1000) + 60 * 60 * 24  // 24時間
    },
    JWT_SECRET
  )

  return c.json({ token })
})

// 保護されたルート
app.use('/api/protected/*', jwt({ secret: JWT_SECRET }))

app.get('/api/protected/profile', async (c) => {
  const payload = c.get('jwtPayload')

  const user = await c.env.DB.prepare(
    'SELECT id, name, email FROM users WHERE id = ?'
  ).bind(payload.sub).first()

  return c.json(user)
})

ベーシック認証

import { basicAuth } from 'hono/basic-auth'

app.use('/admin/*', basicAuth({
  username: 'admin',
  password: 'secret',
  realm: 'Admin Area'
}))

app.get('/admin/dashboard', (c) => {
  return c.json({ message: 'Welcome to admin dashboard' })
})

API Key認証

app.use('/api/*', async (c, next) => {
  const apiKey = c.req.header('x-api-key')

  if (!apiKey) {
    return c.json({ error: 'API key required' }, 401)
  }

  const valid = await c.env.CACHE.get(`apikey:${apiKey}`)

  if (!valid) {
    return c.json({ error: 'Invalid API key' }, 401)
  }

  await next()
})

ミドルウェア

CORS設定

import { cors } from 'hono/cors'

// シンプル
app.use('/*', cors())

// カスタム
app.use('/api/*', cors({
  origin: ['https://example.com', 'https://app.example.com'],
  allowMethods: ['GET', 'POST', 'PUT', 'DELETE'],
  allowHeaders: ['Content-Type', 'Authorization'],
  credentials: true,
  maxAge: 3600
}))

レート制限

app.use('/api/*', async (c, next) => {
  const ip = c.req.header('cf-connecting-ip') || 'unknown'
  const key = `ratelimit:${ip}`

  const count = await c.env.CACHE.get(key)

  if (count && Number(count) >= 100) {
    return c.json({ error: 'Rate limit exceeded' }, 429)
  }

  await c.env.CACHE.put(key, String(Number(count || 0) + 1), {
    expirationTtl: 60  // 1分
  })

  await next()
})

ロギング

import { logger } from 'hono/logger'

app.use('*', logger())

// カスタムロガー
app.use('*', async (c, next) => {
  const start = Date.now()
  await next()
  const duration = Date.now() - start

  console.log({
    method: c.req.method,
    path: c.req.path,
    status: c.res.status,
    duration,
    ip: c.req.header('cf-connecting-ip'),
    country: c.req.header('cf-ipcountry')
  })
})

エラーハンドリング

app.onError((err, c) => {
  console.error(err)

  if (err instanceof Error) {
    return c.json({
      error: err.message,
      stack: process.env.NODE_ENV === 'development' ? err.stack : undefined
    }, 500)
  }

  return c.json({ error: 'Internal server error' }, 500)
})

app.notFound((c) => {
  return c.json({ error: 'Not found' }, 404)
})

バリデーション

Zod統合

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

const UserSchema = z.object({
  name: z.string().min(1).max(100),
  email: z.string().email(),
  age: z.number().min(0).max(150).optional()
})

app.post('/api/users', zValidator('json', UserSchema), async (c) => {
  const user = c.req.valid('json')

  const result = await c.env.DB.prepare(
    'INSERT INTO users (name, email) VALUES (?, ?)'
  ).bind(user.name, user.email).run()

  return c.json({
    id: result.meta.last_row_id,
    ...user
  }, 201)
})

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

const SearchSchema = z.object({
  q: z.string().min(1),
  page: z.string().regex(/^\d+$/).transform(Number).default('1'),
  limit: z.string().regex(/^\d+$/).transform(Number).default('10')
})

app.get('/api/search', zValidator('query', SearchSchema), async (c) => {
  const { q, page, limit } = c.req.valid('query')
  const offset = (page - 1) * limit

  const { results } = await c.env.DB.prepare(`
    SELECT * FROM posts
    WHERE title LIKE ? OR content LIKE ?
    LIMIT ? OFFSET ?
  `).bind(`%${q}%`, `%${q}%`, limit, offset).all()

  return c.json(results)
})

実践例: ブログAPI

完全な実装例:

import { Hono } from 'hono'
import { cors } from 'hono/cors'
import { logger } from 'hono/logger'
import { jwt, sign } from 'hono/jwt'
import { zValidator } from '@hono/zod-validator'
import { z } from 'zod'

type Bindings = {
  DB: D1Database
  CACHE: KVNamespace
  BUCKET: R2Bucket
}

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

// ミドルウェア
app.use('*', logger())
app.use('*', cors())

const JWT_SECRET = 'your-secret-key'

// スキーマ
const RegisterSchema = z.object({
  name: z.string().min(1),
  email: z.string().email(),
  password: z.string().min(8)
})

const LoginSchema = z.object({
  email: z.string().email(),
  password: z.string()
})

const PostSchema = z.object({
  title: z.string().min(1).max(200),
  content: z.string().min(1),
  published: z.boolean().default(false)
})

// 認証
app.post('/api/register', zValidator('json', RegisterSchema), async (c) => {
  const { name, email, password } = c.req.valid('json')

  const result = await c.env.DB.prepare(
    'INSERT INTO users (name, email, password) VALUES (?, ?, ?)'
  ).bind(name, email, password).run()

  return c.json({ id: result.meta.last_row_id }, 201)
})

app.post('/api/login', zValidator('json', LoginSchema), async (c) => {
  const { email, password } = c.req.valid('json')

  const user = await c.env.DB.prepare(
    'SELECT * FROM users WHERE email = ? AND password = ?'
  ).bind(email, password).first()

  if (!user) {
    return c.json({ error: 'Invalid credentials' }, 401)
  }

  const token = await sign(
    { sub: user.id, exp: Math.floor(Date.now() / 1000) + 86400 },
    JWT_SECRET
  )

  return c.json({ token })
})

// 公開API
app.get('/api/posts', async (c) => {
  const cacheKey = 'posts:public'

  // キャッシュチェック
  const cached = await c.env.CACHE.get(cacheKey, 'json')
  if (cached) {
    return c.json(cached)
  }

  const { results } = await c.env.DB.prepare(`
    SELECT
      posts.id,
      posts.title,
      posts.content,
      posts.created_at,
      users.name as author
    FROM posts
    JOIN users ON posts.user_id = users.id
    WHERE posts.published = 1
    ORDER BY posts.created_at DESC
  `).all()

  // キャッシュ保存(5分)
  await c.env.CACHE.put(cacheKey, JSON.stringify(results), {
    expirationTtl: 300
  })

  return c.json(results)
})

app.get('/api/posts/:id', async (c) => {
  const id = c.req.param('id')

  const post = await c.env.DB.prepare(`
    SELECT
      posts.*,
      users.name as author
    FROM posts
    JOIN users ON posts.user_id = users.id
    WHERE posts.id = ? AND posts.published = 1
  `).bind(id).first()

  if (!post) {
    return c.json({ error: 'Not found' }, 404)
  }

  return c.json(post)
})

// 保護されたAPI
app.use('/api/posts', jwt({ secret: JWT_SECRET }))

app.post('/api/posts', zValidator('json', PostSchema), async (c) => {
  const payload = c.get('jwtPayload')
  const { title, content, published } = c.req.valid('json')

  const result = await c.env.DB.prepare(
    'INSERT INTO posts (user_id, title, content, published) VALUES (?, ?, ?, ?)'
  ).bind(payload.sub, title, content, published ? 1 : 0).run()

  // キャッシュクリア
  await c.env.CACHE.delete('posts:public')

  return c.json({ id: result.meta.last_row_id }, 201)
})

export default app

まとめ

Hono + Cloudflare Workersは2026年現在、最もコスパの高いWebアプリケーションスタックの一つです。

メリット

  • 超高速: エッジでのレスポンスタイム10ms以下
  • スケーラブル: 自動スケーリング、設定不要
  • 低コスト: 無料枠で十分な規模をカバー
  • 開発体験: TypeScript完全対応、型安全
  • グローバル: 世界中で低レイテンシ

ユースケース

  • REST API / GraphQL API
  • 認証サーバー
  • リバースプロキシ
  • 画像リサイズ・最適化
  • エッジSSR
  • Webhookハンドラー

無料枠内で本番運用できるため、個人開発からスタートアップまで幅広く使えます。