Hono Webフレームワーク実践ガイド - 超高速エッジランタイム対応の新世代フレームワーク


Honoは、Cloudflare Workers、Deno、Bunなどのエッジランタイムで動作する超軽量なWebフレームワークです。従来のNode.jsフレームワークとは異なり、エッジコンピューティングに最適化された設計で、高速なレスポンスと低いコールドスタート時間を実現します。

本記事では、Honoの基本から実践的な使い方まで、実際のコード例を交えて詳しく解説します。

Honoの特徴

1. マルチランタイム対応

Honoは以下のランタイムで動作します:

  • Cloudflare Workers - グローバルエッジネットワーク
  • Deno - セキュアなTypeScriptランタイム
  • Bun - 高速なJavaScriptランタイム
  • Node.js - 従来のサーバー環境
  • AWS Lambda - サーバーレス環境
  • Vercel - フロントエンドプラットフォーム

2. 超軽量・高速

  • バンドルサイズわずか 13KB
  • Express比で 10倍以上の速度
  • TypeScript完全対応

3. 豊富なミドルウェア

認証、CORS、ロギング、キャッシュなど、実用的なミドルウェアが標準で用意されています。

セットアップ

Cloudflare Workersでの環境構築

# プロジェクト作成
npm create hono@latest my-app

# テンプレート選択
? Which template do you want to use? cloudflare-workers

# ディレクトリ移動
cd my-app

# 依存関係インストール
npm install

# 開発サーバー起動
npm run dev

Bunでの環境構築

# Bunでプロジェクト作成
bun create hono my-app

# ディレクトリ移動
cd my-app

# 開発サーバー起動
bun run dev

基本的なルーティング

シンプルなAPI

import { Hono } from 'hono'

const app = new Hono()

// GET リクエスト
app.get('/', (c) => {
  return c.text('Hello Hono!')
})

// JSON レスポンス
app.get('/api/users', (c) => {
  return c.json({
    users: [
      { id: 1, name: 'Alice' },
      { id: 2, name: 'Bob' }
    ]
  })
})

// パスパラメータ
app.get('/api/users/:id', (c) => {
  const id = c.req.param('id')
  return c.json({ id, name: 'Alice' })
})

// クエリパラメータ
app.get('/search', (c) => {
  const query = c.req.query('q')
  return c.json({ query, results: [] })
})

export default app

RESTful API

import { Hono } from 'hono'

const app = new Hono()

// リソースルーティング
app.get('/api/posts', (c) => c.json({ posts: [] }))
app.post('/api/posts', (c) => c.json({ message: 'Created' }, 201))
app.get('/api/posts/:id', (c) => c.json({ id: c.req.param('id') }))
app.put('/api/posts/:id', (c) => c.json({ message: 'Updated' }))
app.delete('/api/posts/:id', (c) => c.json({ message: 'Deleted' }))

export default app

ミドルウェアの活用

ビルトインミドルウェア

import { Hono } from 'hono'
import { logger } from 'hono/logger'
import { cors } from 'hono/cors'
import { bearerAuth } from 'hono/bearer-auth'
import { cache } from 'hono/cache'

const app = new Hono()

// ロガー
app.use('*', logger())

// CORS設定
app.use('*', cors({
  origin: ['https://example.com'],
  allowMethods: ['GET', 'POST', 'PUT', 'DELETE'],
  allowHeaders: ['Content-Type', 'Authorization'],
  credentials: true
}))

// 認証
app.use('/api/*', bearerAuth({
  token: process.env.API_TOKEN || 'secret-token'
}))

// キャッシュ
app.get('/api/public/*', cache({
  cacheName: 'my-cache',
  cacheControl: 'max-age=3600'
}))

export default app

カスタムミドルウェア

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

const app = new Hono()

// リクエストタイム測定
const timing: MiddlewareHandler = async (c, next) => {
  const start = Date.now()
  await next()
  const end = Date.now()
  c.res.headers.set('X-Response-Time', `${end - start}ms`)
}

// エラーハンドリング
const errorHandler: MiddlewareHandler = async (c, next) => {
  try {
    await next()
  } catch (err) {
    console.error(err)
    return c.json({ error: 'Internal Server Error' }, 500)
  }
}

app.use('*', timing)
app.use('*', errorHandler)

export default app

バリデーション

Zodとの統合

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

const app = new Hono()

// スキーマ定義
const userSchema = z.object({
  name: z.string().min(1).max(50),
  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')
    // userは型安全
    return c.json({
      message: 'User created',
      user
    }, 201)
  }
)

// クエリパラメータのバリデーション
const searchSchema = z.object({
  q: z.string().min(1),
  page: z.string().regex(/^\d+$/).transform(Number).optional(),
  limit: z.string().regex(/^\d+$/).transform(Number).optional()
})

app.get('/api/search',
  zValidator('query', searchSchema),
  async (c) => {
    const { q, page = 1, limit = 10 } = c.req.valid('query')
    return c.json({ q, page, limit, results: [] })
  }
)

export default app

JWT認証

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

const app = new Hono()

const JWT_SECRET = process.env.JWT_SECRET || 'your-secret-key'

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

  // 認証ロジック(実際はDBと照合)
  if (email === 'user@example.com' && password === 'password') {
    const payload = {
      sub: '1234567890',
      email,
      exp: Math.floor(Date.now() / 1000) + 60 * 60 // 1時間
    }

    const token = await sign(payload, JWT_SECRET)
    return c.json({ token })
  }

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

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

app.get('/api/protected/profile', (c) => {
  const payload = c.get('jwtPayload')
  return c.json({
    message: 'Protected data',
    user: payload
  })
})

export default app

データベース統合

Cloudflare D1(SQLite)

import { Hono } from 'hono'

type Bindings = {
  DB: D1Database
}

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

// データ取得
app.get('/api/users', async (c) => {
  const { results } = await c.env.DB.prepare(
    'SELECT * FROM users'
  ).all()

  return c.json({ users: results })
})

// データ挿入
app.post('/api/users', async (c) => {
  const { name, email } = await c.req.json()

  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,
    message: 'User created'
  }, 201)
})

export default app

Cloudflare KV(キーバリューストア)

import { Hono } from 'hono'

type Bindings = {
  KV: 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.KV.get(key)

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

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

// データ保存
app.put('/api/cache/:key', async (c) => {
  const key = c.req.param('key')
  const { value, ttl } = await c.req.json()

  await c.env.KV.put(key, value, { expirationTtl: ttl || 3600 })

  return c.json({ message: 'Cached' })
})

export default app

ファイルアップロード

import { Hono } from 'hono'

const app = new Hono()

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 info = {
    name: file.name,
    type: file.type,
    size: file.size
  }

  // バッファ取得
  const buffer = await file.arrayBuffer()

  // Cloudflare R2にアップロード(例)
  // await c.env.R2.put(file.name, buffer)

  return c.json({
    message: 'File uploaded',
    file: info
  })
})

export default app

テスト

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

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

  it('GET /api/users', async () => {
    const res = await app.request('/api/users')
    expect(res.status).toBe(200)

    const json = await res.json()
    expect(json).toHaveProperty('users')
    expect(Array.isArray(json.users)).toBe(true)
  })

  it('POST /api/users - validation error', async () => {
    const res = await app.request('/api/users', {
      method: 'POST',
      headers: { 'Content-Type': 'application/json' },
      body: JSON.stringify({ name: '' })
    })

    expect(res.status).toBe(400)
  })
})

デプロイ

Cloudflare Workersへのデプロイ

# ログイン
npx wrangler login

# デプロイ
npm run deploy

wrangler.toml設定

name = "my-hono-app"
main = "src/index.ts"
compatibility_date = "2024-01-01"

# KV Namespace
[[kv_namespaces]]
binding = "KV"
id = "your-kv-id"

# D1 Database
[[d1_databases]]
binding = "DB"
database_name = "your-db-name"
database_id = "your-db-id"

# R2 Bucket
[[r2_buckets]]
binding = "R2"
bucket_name = "your-bucket-name"

パフォーマンス最適化

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

app.get('/api/stream', (c) => {
  return c.streamText(async (stream) => {
    for (let i = 0; i < 10; i++) {
      await stream.write(`Chunk ${i}\n`)
      await stream.sleep(100)
    }
  })
})

キャッシュ戦略

import { cache } from 'hono/cache'

// エッジキャッシュ
app.get('/api/public/data',
  cache({
    cacheName: 'public-api',
    cacheControl: 'public, max-age=3600, s-maxage=86400'
  }),
  async (c) => {
    // 重い処理
    const data = await fetchExpensiveData()
    return c.json(data)
  }
)

まとめ

Honoは、エッジコンピューティング時代の新しい選択肢として非常に有望なフレームワークです。主な利点:

  • 超軽量で高速 - コールドスタートが速い
  • マルチランタイム対応 - プラットフォーム非依存
  • TypeScript完全対応 - 型安全な開発
  • 豊富なミドルウェア - 実用的な機能がすぐ使える
  • シンプルなAPI - 学習コストが低い

Express、Fastify、Koaなどの従来フレームワークからの移行も容易で、エッジ環境でのAPI開発に最適です。

次世代のWebアプリケーション開発に、Honoをぜひ試してみてください。

参考リンク