ミドルウェア活用
ミドルウェアは、リクエストとレスポンスの間で実行される処理のことです。認証、ログ出力、CORS設定、エラーハンドリングなど、アプリケーション全体で共通して必要な機能を効率的に実装できます。Honoのミドルウェアシステムについて詳しく学んでいきましょう。
ミドルウェアの基本概念
ミドルウェアの動作流れ
import { Hono } from 'hono'
const app = new Hono()
// ミドルウェア1(前処理)
app.use('*', async (c, next) => {
console.log('Before request processing')
await next() // 次の処理へ
console.log('After request processing')
})
// ミドルウェア2(認証)
app.use('/api/*', async (c, next) => {
console.log('Authentication check')
await next()
})
// ルートハンドラー
app.get('/api/data', (c) => {
console.log('Route handler')
return c.json({ message: 'Hello' })
})
export default app
実行順序:
- Before request processing
- Authentication check
- Route handler
- After request processing
「next()を呼ぶことで、次の処理に制御を渡せます。」
ミドルウェアの種類
// 1. 全ての経路に適用
app.use('*', middleware)
// 2. 特定のパスに適用
app.use('/api/*', middleware)
// 3. 特定のメソッドとパスに適用
app.use('GET', '/admin/*', middleware)
// 4. 複数パスに適用
app.use(['/api/*', '/admin/*'], middleware)
組み込みミドルウェア
Honoには便利な組み込みミドルウェアが用意されています。
1. Logger(ログ出力)
import { Hono } from 'hono'
import { logger } from 'hono/logger'
const app = new Hono()
app.use('*', logger())
app.get('/', (c) => c.text('Hello'))
// コンソール出力例:
// GET / 200 - 2.34ms
2. CORS(Cross-Origin Resource Sharing)
import { cors } from 'hono/cors'
// 基本的な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
}))
// 動的なorigin設定
app.use('/api/*', cors({
origin: (origin) => {
return origin?.endsWith('.example.com') ? origin : 'https://example.com'
}
}))
3. JWT認証
import { jwt } from 'hono/jwt'
app.use('/api/protected/*', jwt({
secret: 'your-secret-key'
}))
// JWTが検証されたルート
app.get('/api/protected/user', (c) => {
const payload = c.get('jwtPayload')
return c.json({ user: payload })
})
4. Basic認証
import { basicAuth } from 'hono/basic-auth'
app.use('/admin/*', basicAuth({
username: 'admin',
password: 'secret'
}))
// 複数ユーザー対応
app.use('/admin/*', basicAuth({
verifyUser: (username, password, c) => {
return username === 'admin' && password === 'secret123' ||
username === 'user' && password === 'user123'
}
}))
5. プリティ印刷
import { prettyJSON } from 'hono/pretty-json'
app.use('*', prettyJSON())
app.get('/api/data', (c) => {
return c.json({
users: [
{ id: 1, name: 'Alice' },
{ id: 2, name: 'Bob' }
]
})
})
// JSONが整形されて出力される
6. セキュアヘッダー
import { secureHeaders } from 'hono/secure-headers'
app.use('*', secureHeaders())
// カスタム設定
app.use('*', secureHeaders({
contentSecurityPolicy: {
defaultSrc: ["'self'"],
styleSrc: ["'self'", "'unsafe-inline'"],
scriptSrc: ["'self'"]
},
crossOriginEmbedderPolicy: false
}))
カスタムミドルウェアの作成
基本的なミドルウェア
// リクエスト時刻を記録するミドルウェア
const timing = async (c: any, next: any) => {
const start = Date.now()
await next()
const end = Date.now()
console.log(`Request took ${end - start}ms`)
}
app.use('*', timing)
リクエストIDミドルウェア
const requestId = async (c: any, next: any) => {
const id = crypto.randomUUID()
c.set('requestId', id)
c.header('X-Request-ID', id)
await next()
}
app.use('*', requestId)
app.get('/test', (c) => {
const requestId = c.get('requestId')
return c.json({ requestId })
})
API制限ミドルウェア(Rate Limiting)
interface RateLimit {
count: number
resetTime: number
}
const rateLimitMap = new Map<string, RateLimit>()
const rateLimit = (maxRequests: number, windowMs: number) => {
return async (c: any, next: any) => {
const ip = c.req.header('CF-Connecting-IP') ||
c.req.header('X-Forwarded-For') ||
'unknown'
const now = Date.now()
const limit = rateLimitMap.get(ip)
if (!limit || now > limit.resetTime) {
// 新しいウィンドウ
rateLimitMap.set(ip, {
count: 1,
resetTime: now + windowMs
})
await next()
} else if (limit.count < maxRequests) {
// まだ制限内
limit.count++
await next()
} else {
// 制限超過
return c.json({
error: 'Too many requests',
retryAfter: Math.ceil((limit.resetTime - now) / 1000)
}, 429)
}
}
}
// 使用例:1分間に10回まで
app.use('/api/*', rateLimit(10, 60 * 1000))
キャッシュミドルウェア
const cache = new Map<string, { data: any, expires: number }>()
const cacheMiddleware = (ttlSeconds: number) => {
return async (c: any, next: any) => {
const key = `${c.req.method}:${c.req.url}`
const cached = cache.get(key)
if (cached && cached.expires > Date.now()) {
// キャッシュヒット
c.header('X-Cache', 'HIT')
return c.json(cached.data)
}
// 元の処理を実行
await next()
// レスポンスをキャッシュ(JSONの場合のみ)
const response = c.res
if (response.headers.get('content-type')?.includes('application/json')) {
const data = await response.clone().json()
cache.set(key, {
data,
expires: Date.now() + (ttlSeconds * 1000)
})
c.header('X-Cache', 'MISS')
}
}
}
app.use('/api/cache/*', cacheMiddleware(300)) // 5分間キャッシュ
エラーハンドリングミドルウェア
グローバルエラーハンドラー
const errorHandler = async (c: any, next: any) => {
try {
await next()
} catch (error) {
console.error('Global error:', error)
if (error instanceof z.ZodError) {
return c.json({
error: 'Validation Error',
details: error.errors
}, 400)
}
if (error.message === 'Unauthorized') {
return c.json({ error: 'Authentication required' }, 401)
}
return c.json({
error: 'Internal Server Error',
message: 'An unexpected error occurred'
}, 500)
}
}
app.use('*', errorHandler)
非同期エラーのキャッチ
const asyncHandler = (fn: Function) => {
return async (c: any, next: any) => {
try {
await fn(c, next)
} catch (error) {
// エラーを次のエラーハンドラーに渡す
throw error
}
}
}
// 使用例
app.get('/api/data', asyncHandler(async (c) => {
const data = await riskyAsyncOperation()
return c.json(data)
}))
ミドルウェアの組み合わせ
複数のミドルウェアを効果的に組み合わせる例:
import { Hono } from 'hono'
import { logger } from 'hono/logger'
import { cors } from 'hono/cors'
import { jwt } from 'hono/jwt'
import { prettyJSON } from 'hono/pretty-json'
const app = new Hono()
// 1. 全体的なミドルウェア
app.use('*', logger())
app.use('*', cors({
origin: ['http://localhost:3000', 'https://myapp.com'],
credentials: true
}))
app.use('*', prettyJSON())
// 2. API用のミドルウェア
app.use('/api/*', async (c, next) => {
c.header('X-API-Version', 'v1.0.0')
await next()
})
// 3. 保護されたルート用のミドルウェア
app.use('/api/protected/*', jwt({ secret: 'secret' }))
app.use('/api/protected/*', rateLimit(100, 60 * 1000)) // 分間100リクエスト
// 4. 管理者用のミドルウェア
app.use('/api/admin/*', basicAuth({
username: 'admin',
password: 'secret'
}))
// ルート定義
app.get('/api/public', (c) => c.json({ message: 'Public API' }))
app.get('/api/protected/user', (c) => c.json({ message: 'Protected API' }))
app.get('/api/admin/stats', (c) => c.json({ message: 'Admin API' }))
export default app
条件付きミドルウェア
特定の条件でのみミドルウェアを実行:
const conditionalAuth = async (c: any, next: any) => {
const path = c.req.url
const method = c.req.method
// GET リクエストは認証不要
if (method === 'GET') {
await next()
return
}
// POST/PUT/DELETE は認証必須
const token = c.req.header('Authorization')
if (!token) {
return c.json({ error: 'Authorization required' }, 401)
}
// トークン検証...
await next()
}
app.use('/api/posts/*', conditionalAuth)
ミドルウェアのテスト
import { describe, it, expect } from 'vitest'
describe('Rate Limit Middleware', () => {
it('should allow requests within limit', async () => {
const app = new Hono()
app.use('*', rateLimit(2, 1000))
app.get('/', (c) => c.json({ ok: true }))
// 最初のリクエスト
const res1 = await app.request('/')
expect(res1.status).toBe(200)
// 2回目のリクエスト
const res2 = await app.request('/')
expect(res2.status).toBe(200)
// 3回目のリクエスト(制限超過)
const res3 = await app.request('/')
expect(res3.status).toBe(429)
})
})
パフォーマンス考慮事項
ミドルウェアの順序
// ❌ 非効率な順序
app.use('*', heavyComputationMiddleware) // 重い処理
app.use('/api/*', authMiddleware) // 認証
// ✅ 効率的な順序
app.use('/api/*', authMiddleware) // 認証(早期リターン可能)
app.use('/api/*', heavyComputationMiddleware) // 重い処理
メモリリーク対策
// ❌ メモリリークの可能性
const globalCache = new Map()
const badCacheMiddleware = async (c: any, next: any) => {
globalCache.set(c.req.url, 'some data') // 無制限にデータが蓄積
await next()
}
// ✅ サイズ制限付きキャッシュ
class LRUCache<K, V> {
private cache = new Map<K, V>()
constructor(private maxSize: number) {}
set(key: K, value: V) {
if (this.cache.size >= this.maxSize) {
const firstKey = this.cache.keys().next().value
this.cache.delete(firstKey)
}
this.cache.set(key, value)
}
get(key: K): V | undefined {
const value = this.cache.get(key)
if (value !== undefined) {
// LRU: 最近使用したものを末尾に移動
this.cache.delete(key)
this.cache.set(key, value)
}
return value
}
}
const cache = new LRUCache<string, any>(1000)
やってみよう!
実践的なミドルウェアを作成してみましょう:
-
アクセス統計ミドルウェア
- エンドポイントごとのアクセス数を記録
- 統計情報をAPIで取得可能
-
リクエストサイズ制限ミドルウェア
- JSONペイロードのサイズを制限
- 大きすぎるリクエストを拒否
-
セッションミドルウェア
- シンプルなセッション管理機能
- メモリ内でセッションを管理
ポイント
- ミドルウェアチェーン:
next()による処理の連携 - 組み込みミドルウェア:認証、CORS、ログなどの便利な機能
- カスタムミドルウェア:アプリケーション固有の処理を共通化
- エラーハンドリング:グローバルなエラー処理の実装
- パフォーマンス:ミドルウェアの順序とメモリ使用量の最適化