Honoで始めるエッジフレームワーク開発 - 超軽量で高速なWebアプリケーション
Honoで始めるエッジフレームワーク開発
HonoはCloudflare Workers、Deno、Bun、Node.jsなど、あらゆるJavaScriptランタイムで動作する超軽量Webフレームワークです。エッジコンピューティングに最適化されており、驚異的な速度とシンプルなAPIが特徴です。
Honoとは
Honoは「炎」を意味する日本語から名付けられた、次世代のWebフレームワークです。主な特徴は以下の通りです。
- 超軽量: バンドルサイズが約12KB
- 高速: ミドルウェア処理が最適化され、Express.jsの約10倍高速
- マルチランタイム対応: Cloudflare Workers、Deno、Bun、Node.js、AWS Lambdaなど
- TypeScript完全対応: 型安全なAPI設計
- 豊富なミドルウェア: JWT、CORS、圧縮、キャッシュなど
セットアップ
Cloudflare Workersでの基本セットアップ
npm create hono@latest my-hono-app
cd my-hono-app
npm install
npm run dev
プロジェクト作成時に以下のオプションを選択できます。
? Which template do you want to use?
cloudflare-workers
cloudflare-pages
deno
bun
nodejs
aws-lambda
最小限のアプリケーション
import { Hono } from 'hono'
const app = new Hono()
app.get('/', (c) => {
return c.text('Hello Hono!')
})
export default app
ルーティング
基本的なルーティング
import { Hono } from 'hono'
const app = new Hono()
// GET
app.get('/posts', (c) => c.json({ posts: [] }))
// POST
app.post('/posts', async (c) => {
const body = await c.req.json()
return c.json({ id: 1, ...body }, 201)
})
// PUT
app.put('/posts/:id', async (c) => {
const id = c.req.param('id')
const body = await c.req.json()
return c.json({ id, ...body })
})
// DELETE
app.delete('/posts/:id', (c) => {
const id = c.req.param('id')
return c.json({ deleted: id })
})
export default app
パスパラメータとクエリ
// パスパラメータ
app.get('/users/:id', (c) => {
const id = c.req.param('id')
return c.json({ userId: id })
})
// 複数のパラメータ
app.get('/posts/:postId/comments/:commentId', (c) => {
const { postId, commentId } = c.req.param()
return c.json({ postId, commentId })
})
// クエリパラメータ
app.get('/search', (c) => {
const query = c.req.query('q')
const page = c.req.query('page') ?? '1'
return c.json({ query, page })
})
// ワイルドカード
app.get('/files/*', (c) => {
const path = c.req.param('*')
return c.text(`File path: ${path}`)
})
ルートグループ化
const app = new Hono()
// APIルートのグループ化
const api = new Hono()
api.get('/users', (c) => c.json([]))
api.get('/posts', (c) => c.json([]))
app.route('/api/v1', api)
// さらにネスト
const admin = new Hono()
admin.get('/dashboard', (c) => c.text('Admin Dashboard'))
api.route('/admin', admin)
// 結果: /api/v1/admin/dashboard
ミドルウェア
ビルトインミドルウェア
import { Hono } from 'hono'
import { logger } from 'hono/logger'
import { cors } from 'hono/cors'
import { compress } from 'hono/compress'
import { etag } from 'hono/etag'
import { cache } from 'hono/cache'
const app = new Hono()
// ロギング
app.use('*', logger())
// CORS設定
app.use('*', cors({
origin: ['https://example.com', 'https://app.example.com'],
allowMethods: ['GET', 'POST', 'PUT', 'DELETE'],
credentials: true,
}))
// レスポンス圧縮
app.use('*', compress())
// ETag生成
app.use('*', etag())
// キャッシュ制御
app.get('/api/static-data',
cache({
cacheName: 'my-cache',
cacheControl: 'max-age=3600',
}),
(c) => {
return c.json({ data: 'cached response' })
}
)
カスタムミドルウェア
// リクエスト時間計測
const timing = async (c, next) => {
const start = Date.now()
await next()
const end = Date.now()
c.res.headers.set('X-Response-Time', `${end - start}ms`)
}
app.use('*', timing)
// 認証ミドルウェア
const authMiddleware = async (c, next) => {
const token = c.req.header('Authorization')
if (!token) {
return c.json({ error: 'Unauthorized' }, 401)
}
try {
const user = await verifyToken(token)
c.set('user', user)
await next()
} catch (error) {
return c.json({ error: 'Invalid token' }, 401)
}
}
app.use('/api/protected/*', authMiddleware)
app.get('/api/protected/profile', (c) => {
const user = c.get('user')
return c.json({ user })
})
JWT認証
import { Hono } from 'hono'
import { jwt } from 'hono/jwt'
const app = new Hono()
// JWT検証ミドルウェア
app.use('/api/protected/*', jwt({
secret: 'your-secret-key',
}))
// ログインエンドポイント
app.post('/api/login', async (c) => {
const { username, password } = await c.req.json()
// 認証ロジック(省略)
const payload = {
sub: username,
exp: Math.floor(Date.now() / 1000) + 60 * 60, // 1時間
}
const token = await sign(payload, 'your-secret-key')
return c.json({ token })
})
// 保護されたエンドポイント
app.get('/api/protected/data', (c) => {
const payload = c.get('jwtPayload')
return c.json({
message: 'Protected data',
user: payload.sub
})
})
バリデーション
Zodを使ったバリデーション
import { Hono } from 'hono'
import { zValidator } from '@hono/zod-validator'
import { z } from 'zod'
const app = new Hono()
const postSchema = z.object({
title: z.string().min(1).max(100),
content: z.string().min(10),
tags: z.array(z.string()).optional(),
published: z.boolean().default(false),
})
app.post('/posts',
zValidator('json', postSchema),
async (c) => {
const data = c.req.valid('json')
// dataは型安全
// { title: string, content: string, tags?: string[], published: boolean }
return c.json({
success: true,
post: data
}, 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('/search',
zValidator('query', searchSchema),
(c) => {
const { q, page = 1, limit = 10 } = c.req.valid('query')
return c.json({ q, page, limit })
}
)
データベース接続
Cloudflare D1(SQLite)の利用
import { Hono } from 'hono'
type Bindings = {
DB: D1Database
}
const app = new Hono<{ Bindings: Bindings }>()
app.get('/users', async (c) => {
const { results } = await c.env.DB.prepare(
'SELECT * FROM users ORDER BY created_at DESC LIMIT ?'
).bind(10).all()
return c.json({ users: results })
})
app.post('/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,
name,
email
}, 201)
})
app.get('/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 })
})
Cloudflare KV(Key-Value Store)
type Bindings = {
KV: KVNamespace
}
const app = new Hono<{ Bindings: Bindings }>()
// キャッシュとしてKVを使用
app.get('/api/expensive-data', async (c) => {
const cacheKey = 'expensive-data-v1'
// キャッシュから取得
const cached = await c.env.KV.get(cacheKey, 'json')
if (cached) {
return c.json(cached)
}
// データ生成(重い処理)
const data = await generateExpensiveData()
// KVに保存(TTL: 1時間)
await c.env.KV.put(cacheKey, JSON.stringify(data), {
expirationTtl: 3600,
})
return c.json(data)
})
// セッション管理
app.post('/api/session', async (c) => {
const sessionId = crypto.randomUUID()
const sessionData = { userId: 123, createdAt: Date.now() }
await c.env.KV.put(`session:${sessionId}`, JSON.stringify(sessionData), {
expirationTtl: 86400, // 24時間
})
return c.json({ sessionId })
})
RPCモード(型安全なクライアント通信)
Honoの強力な機能の一つが、型安全なRPCクライアントです。
サーバー側
// server.ts
import { Hono } from 'hono'
const app = new Hono()
const routes = app
.get('/api/posts', (c) => {
return c.json([
{ id: 1, title: 'Post 1' },
{ id: 2, title: 'Post 2' },
])
})
.post('/api/posts', async (c) => {
const { title, content } = await c.req.json()
return c.json({ id: 3, title, content }, 201)
})
export type AppType = typeof routes
export default app
クライアント側
// client.ts
import { hc } from 'hono/client'
import type { AppType } from './server'
const client = hc<AppType>('http://localhost:8787')
// 完全に型安全
const res = await client.api.posts.$get()
const posts = await res.json()
// posts: { id: number, title: string }[]
// POSTリクエストも型安全
const createRes = await client.api.posts.$post({
json: {
title: 'New Post',
content: 'Content here',
}
})
const newPost = await createRes.json()
エラーハンドリング
import { Hono } from 'hono'
import { HTTPException } from 'hono/http-exception'
const app = new Hono()
// カスタムエラーレスポンス
app.onError((err, c) => {
if (err instanceof HTTPException) {
return c.json({
error: err.message,
status: err.status,
}, err.status)
}
console.error(err)
return c.json({
error: 'Internal Server Error',
}, 500)
})
// HTTPExceptionの使用
app.get('/users/:id', async (c) => {
const id = c.req.param('id')
const user = await findUser(id)
if (!user) {
throw new HTTPException(404, {
message: `User ${id} not found`
})
}
return c.json({ user })
})
// 404ハンドラ
app.notFound((c) => {
return c.json({
error: 'Not Found',
path: c.req.path,
}, 404)
})
テスト
import { describe, it, expect } from 'vitest'
import app from './app'
describe('Hono App', () => {
it('should return hello message', async () => {
const res = await app.request('/')
expect(res.status).toBe(200)
const text = await res.text()
expect(text).toBe('Hello Hono!')
})
it('should create a post', async () => {
const res = await app.request('/posts', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({
title: 'Test Post',
content: 'Test Content',
}),
})
expect(res.status).toBe(201)
const data = await res.json()
expect(data).toHaveProperty('id')
expect(data.title).toBe('Test Post')
})
it('should handle errors', async () => {
const res = await app.request('/not-found')
expect(res.status).toBe(404)
})
})
デプロイ
Cloudflare Workersへのデプロイ
# wrangler.tomlの設定
# name = "my-hono-app"
# main = "src/index.ts"
# compatibility_date = "2024-01-01"
npm run deploy
Vercel Edge Functionsへのデプロイ
// api/index.ts
import { Hono } from 'hono'
import { handle } from 'hono/vercel'
const app = new Hono().basePath('/api')
app.get('/hello', (c) => c.text('Hello from Vercel!'))
export const GET = handle(app)
export const POST = handle(app)
まとめ
Honoは次世代のエッジコンピューティングに最適化された、超軽量で高速なWebフレームワークです。主な利点は以下の通りです。
- マルチランタイム対応で柔軟なデプロイ先
- 型安全なRPCモードで開発体験が向上
- 豊富なミドルウェアエコシステム
- Express.jsライクなシンプルなAPI
エッジコンピューティングの普及により、Honoのようなフレームワークは今後ますます重要になっていきます。ぜひ次のプロジェクトで試してみてください。