Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

ミドルウェア活用

ミドルウェアは、リクエストとレスポンスの間で実行される処理のことです。認証、ログ出力、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

実行順序:

  1. Before request processing
  2. Authentication check
  3. Route handler
  4. 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)

やってみよう!

実践的なミドルウェアを作成してみましょう:

  1. アクセス統計ミドルウェア

    • エンドポイントごとのアクセス数を記録
    • 統計情報をAPIで取得可能
  2. リクエストサイズ制限ミドルウェア

    • JSONペイロードのサイズを制限
    • 大きすぎるリクエストを拒否
  3. セッションミドルウェア

    • シンプルなセッション管理機能
    • メモリ内でセッションを管理

ポイント

  • ミドルウェアチェーンnext()による処理の連携
  • 組み込みミドルウェア:認証、CORS、ログなどの便利な機能
  • カスタムミドルウェア:アプリケーション固有の処理を共通化
  • エラーハンドリング:グローバルなエラー処理の実装
  • パフォーマンス:ミドルウェアの順序とメモリ使用量の最適化

参考文献