ミドルウェア

ハンドラの前後で実行される関数。認証・ロギング・キャッシュ・CORS・圧縮等を再利用可能な形で挟める。 (c, next) => ... という Express 風のシグネチャ。

基本

app.use(async (c, next) => {
  console.log(`-> ${c.req.method} ${c.req.path}`)
  await next()                          // 後続を実行
  console.log(`<- ${c.res.status}`)     // 応答後の処理
})

next() の前: 受信時の処理 / 後: 応答時の処理 / 呼ばない: 短絡(ハンドラに行かない)

適用範囲

// 全ルート
app.use(middleware)

// 特定 path
app.use("/api/*", middleware)

// 複数を順に
app.use("/api/*", logger(), cors(), authMiddleware)

// 特定ルート単体
app.get("/admin", auth, (c) => ...)

組み込みミドルウェア(hono/...)

名前用途
loggerリクエストログ
corsCORS ヘッダー
csrfCSRF 検証(Origin ベース)
secure-headersセキュリティヘッダー(X-Frame-Options 等)
basic-authBasic 認証
bearer-authBearer Token 認証
jwtJWT 検証
cacheレスポンスキャッシュ
etagETag 自動付与
compressgzip / deflate
pretty-jsonJSON 整形(?pretty
timingServer-Timing ヘッダー
request-idX-Request-Id 付与
method-overridePOST から PUT/DELETE を擬似
body-limitボディサイズ制限
trailing-slash末尾スラッシュ統一
languageAccept-Language 解析
ip-restrictionIP 制限

logger

import { logger } from "hono/logger"
app.use(logger())

cors

import { cors } from "hono/cors"

app.use("/api/*", cors({
  origin: ["https://example.com", "http://localhost:5173"],
  allowHeaders: ["Authorization", "Content-Type"],
  allowMethods: ["GET", "POST", "PUT", "DELETE"],
  exposeHeaders: ["Content-Length"],
  maxAge: 600,
  credentials: true,
}))

secure-headers

import { secureHeaders } from "hono/secure-headers"

app.use(secureHeaders({
  xFrameOptions: "DENY",
  contentSecurityPolicy: {
    defaultSrc: ["'self'"],
    scriptSrc: ["'self'", "'nonce-...'"],
  },
  strictTransportSecurity: "max-age=31536000; includeSubDomains",
}))

cache(Cloudflare Cache API 連携)

import { cache } from "hono/cache"

app.get("/api/heavy", cache({
  cacheName: "api-v1",
  cacheControl: "max-age=3600",
}), (c) => {
  return c.json({ data: ... })
})

compress

import { compress } from "hono/compress"
app.use(compress())

etag

import { etag } from "hono/etag"
app.use(etag())

自作ミドルウェア

シンプルなロガー

app.use(async (c, next) => {
  const start = Date.now()
  await next()
  const ms = Date.now() - start
  console.log(`${c.req.method} ${c.req.path} ${c.res.status} ${ms}ms`)
})

認証チェック

type Variables = { user: User }
const app = new Hono<{ Variables: Variables }>()

const auth = async (c: any, next: any) => {
  const token = c.req.header("authorization")?.replace("Bearer ", "")
  if (!token) return c.text("Unauthorized", 401)

  try {
    const user = await verifyToken(token)
    c.set("user", user)
    await next()
  } catch {
    return c.text("Unauthorized", 401)
  }
}

app.use("/api/*", auth)
app.get("/api/me", (c) => c.json(c.get("user")))

関数ファクトリパターン

import type { MiddlewareHandler } from "hono"

function rateLimiter(limit: number): MiddlewareHandler {
  const counts = new Map<string, { count: number, reset: number }>()

  return async (c, next) => {
    const ip = c.req.header("cf-connecting-ip") ?? "anon"
    const now = Date.now()
    const record = counts.get(ip) ?? { count: 0, reset: now + 60_000 }

    if (now > record.reset) {
      record.count = 0
      record.reset = now + 60_000
    }
    record.count++
    counts.set(ip, record)

    if (record.count > limit) {
      return c.text("Too Many Requests", 429)
    }
    await next()
  }
}

app.use("/api/*", rateLimiter(60))

レスポンス書き換え

app.use(async (c, next) => {
  await next()
  // 全レスポンスにヘッダーを追加
  c.header("X-Powered-By", "Hono")
})

順序が重要

app.use登録順に実行される。ロガー → CORS → 認証 → ハンドラのように、 外側から内側へ並べる。

app.use(logger())                 // 1. リクエストをログ
app.use(cors())                   // 2. CORS
app.use("/api/*", auth)           // 3. 認証
app.get("/api/data", handler)     // 4. ハンドラ

条件付き分岐

app.use(async (c, next) => {
  if (c.req.method === "OPTIONS") {
    return c.text("", 204)        // preflight は短絡
  }
  await next()
})

onError との連携

ミドルウェア内の例外は app.onError で受けられる。

import { HTTPException } from "hono/http-exception"

app.use("/api/*", async (c, next) => {
  if (!c.req.header("x-api-key")) {
    throw new HTTPException(401, { message: "API Key required" })
  }
  await next()
})

app.onError((err, c) => {
  if (err instanceof HTTPException) {
    return err.getResponse()
  }
  console.error(err)
  return c.json({ error: "Internal Server Error" }, 500)
})

テスト

import { Hono } from "hono"
import { auth } from "./middleware"

const app = new Hono().use(auth).get("/me", (c) => c.text("ok"))

const res = await app.request("/me", { headers: { Authorization: "Bearer valid" } })
expect(res.status).toBe(200)
設計のコツ

✓ ミドルウェアは1責任に保つ
関数ファクトリで設定値を受けるパターンを使う
✓ 認証等の重要な middleware はテストを書く
✓ ログ・メトリクスは executionCtx.waitUntil でレスポンスをブロックしない(CF Workers)