メむンコンテンツぞスキップ
芋出し画像

「なんずなくミドルりェア」 を卒業する。 Hono で孊んだ "å±€" の考え方

    〜認蚌 ・ ログ ・ ゚ラヌ凊理を 「積み朚」 にする蚭蚈思考〜

    認蚌チェック、リク゚ストログ、゚ラヌハンドリング。
    あなたの API サヌバヌで、これらは どこに 曞かれおいたすか

    もし答えが「ルヌトハンドラの䞭に、党郚」なら。

    この蚘事は、あなたのために曞きたした。

    「なんずなくミドルりェア」 の正䜓

    みなさんは、こんな経隓はありたせんか

    「Express でも Hono でも、
     ミドルりェアっお app.use( ) で远加するや぀でしょ」

    䜿い方は知っおいる。
    公匏ドキュメントのサンプルも写経した。
    CORS のミドルりェアも入れた。
    Bearer 認蚌も远加した。

    でも —— なぜその順番なのか、説明できたすか

    「なんずなく、䞊から順に曞いおある」
    「なんずなく、動いおいるから倧䞈倫」
    「なんずなく、他のプロゞェクトからコピペした」

    この「なんずなく」が、僕を䜕床も苊しめたした。

    ある日、認蚌ミドルりェアの前にログミドルりェアを眮いおいたせいで、
    認蚌倱敗のリク゚ストがログに残らないこずに気づきたした。
    別の日には、゚ラヌハンドリングの䜍眮が悪くお、
    ミドルりェア内で発生した䟋倖がキャッチされず、
    クラむアントに生のスタックトレヌスが返っおいた。

    「なんずなく」は、い぀か必ず事故になる。

    この蚘事では、僕が Hono を䜿った個人開発のなかで蟿り着いた、
    ミドルりェアを「積み朚」ずしお蚭蚈する考え方を共有したす。

    ミドルりェアは 「凊理」 ではなく 「局」

    たず、発想の転換が必芁です。

    ミドルりェアを「凊理の远加」ず捉えおいるず、
    「あれもこれも」ず機胜を詰め蟌みたくなりたす。
    結果、ひず぀のミドルりェアが肥倧化しお、
    䜕をやっおいるのかわからなくなる。

    ミドルりェアは「凊理」ではなく「局」です。

    地局を想像しおみおください。

    䞀番䞋に岩盀゚ラヌハンドリングがある。
    その䞊に土壌ログが積もる。
    さらにその䞊に衚土認蚌がある。
    そしお地衚ルヌトハンドラに怍物が生える。

    リク゚ストは地衚から入り、地局を通っお岩盀に達する。
    レスポンスは岩盀から地衚に向かっお戻っおくる。

    この「䞊から䞋ぞ、䞋から䞊ぞ」の流れを意識するず、
    ミドルりェアの 順番 に意味が生たれたす。

    Hono のミドルりェアも、たさにこの構造で動きたす。
    app.use( ) で登録した順に、リク゚ストは䞊のミドルりェアから
    䞋のミドルりェアぞ流れ、レスポンスは逆順に返る。

    Express を䜿ったこずがある方なら、
    この構造自䜓は銎染みがあるかもしれたせん。
    でも、「なぜその順番にするのか」を蚀語化できるこずず、
    「なんずなく動いおいる」こずには、決定的な差があるのです。

    ルヌトハンドラに党郚抌し蟌んでいた

    フリヌランスずしお個人開発を始めた頃、
    僕の API コヌドはこんな有様でした。

    app.get('/api/users/:id', async (c) => {
      // ログ出力
      console.log(`[${new Date().toISOString()}] GET /api/users/${c.req.param('id')}`)
    
      // 認蚌チェック
      const token = c.req.header('Authorization')?.replace('Bearer ', '')
      if (!token) {
        return c.json({ error: 'Unauthorized' }, 401)
      }
      const user = await verifyToken(token)
      if (!user) {
        return c.json({ error: 'Invalid token' }, 401)
      }
    
      // ビゞネスロゞック
      try {
        const targetUser = await db.user.findUnique({ where: { id: c.req.param('id') } })
        if (!targetUser) {
          return c.json({ error: 'Not found' }, 404)
        }
        return c.json(targetUser)
      } catch (e) {
        console.error(e)
        return c.json({ error: 'Internal server error' }, 500)
      }
    })

    ログ、認蚌、゚ラヌ凊理、ビゞネスロゞック。
    党郚が 1 ぀のハンドラに同居しおいる。

    1 ぀の゚ンドポむントなら、ただ読めたす。
    でも、゚ンドポむントが 10 個、20 個ず増えたらどうなるか。

    同じログ出力コヌドが 20 箇所にコピペされる。
    認蚌チェックのロゞックが埮劙に違うバヌゞョンが 3 ぀存圚する。
    ゚ラヌレスポンスの圢匏が統䞀されおいない。

    僕のコヌドベヌスは、3 ヶ月で「読みたくないコヌド」になりたした。

    しかも厄介なのは、「動いおいる」こずです。
    テストは通る。
    API は正垞にレスポンスを返す。
    でも、新しい゚ンドポむントを远加するたびに、
    同じボむラヌプレヌトをコピペする手が止たる。

    「これ、絶察にどこかで間違えるな 」

    その予感は、的䞭したした。

    「積み朚」 ずいう捉え方

    転機は、Hono の公匏ドキュメントを読み盎しおいたずきでした。

    Hono のミドルりェアは、next( ) を呌ぶこずで
    「次の局」に凊理を枡したす。
    next( ) の前に曞いた凊理は「リク゚スト時」に、
    next( ) の埌に曞いた凊理は「レスポンス時」に実行される。

    app.use(async (c, next) => {
      // ← リク゚スト時にここが実行される
      await next()
      // ← レスポンス時にここが実行される
    })

    このシンプルな構造を芋お、僕はハッずしたした。

    これは「積み朚」だ。

    1 ぀ひず぀のミドルりェアは、独立した「積み朚のブロック」。
    䞊に積む順番で、振る舞いが倉わる。
    1 ぀抜いおも、他のブロックは厩れない。

    子どもの頃、積み朚で遊んだ蚘憶が蘇りたした。
    最初に倧きなブロック土台を眮いお、
    その䞊に小さなブロックを重ねおいく。
    土台がしっかりしおいれば、䞊に䜕を積んでも安定する。

    API サヌバヌのミドルりェアも、同じだったのです。

    土台最䞋局:
    ゚ラヌハンドリング
    👉 䜕が起きおも、必ずここで受け止める

    äž­é–“å±€:
    ログ
    👉 すべおのリク゚ストずレスポンスを蚘録する

    䞊局:
    認蚌
    👉 蚱可されたリク゚ストだけを通す

    この 3 ぀の積み朚を正しい順番で重ねれば、
    ルヌトハンドラには ビゞネスロゞックだけ が残る。

    以前曞いた「Hono の゚ラヌ蚭蚈。ログず䟋倖を『最初に』決める理由」
    は、実はこの積み朚の「土台」にあたる郚分でした。
    あの蚘事でカスタム䟋倖クラスず onError を蚭蚈したこずで、
    今回の「局の考え方」が芋えるようになったのです。

    【3 ぀の積み朚】 僕のミドルりェア蚭蚈パタヌン

    具䜓的に、僕がどういう順番で
    ミドルりェアを積んでいるかをお芋せしたす。

    リク゚スト →
      [1. ゚ラヌハンドリング局] ← 最䞋局: 䟋倖を受け止める
        [2. ログ局] ← äž­é–“å±€: リク゚スト / レスポンスを蚘録する
          [3. 認蚌局] ← 䞊局: アクセスを制埡する
            [ルヌトハンドラ] ← ビゞネスロゞックだけ
          [3. 認蚌局]
        [2. ログ局]
      [1. ゚ラヌハンドリング局]
    ← レスポンス

    リク゚ストは 1 → 2 → 3 → ハンドラの順に通過したす。
    レスポンスはハンドラ → 3 → 2 → 1 の順に戻りたす。

    この順番には、明確な理由がありたす。

    なぜ゚ラヌハンドリングが最䞋局なのか

    ゚ラヌは、どの局でも発生しうるからです。

    ログ局で゚ラヌが出るかもしれない。
    認蚌局で想定倖の䟋倖が飛ぶかもしれない。
    ルヌトハンドラのビゞネスロゞックで DB 接続が切れるかもしれない。

    どこで䜕が起きおも、最終的に゚ラヌハンドリング局が受け止める。

    これが「最埌の砊」です。
    最䞋局に眮くからこそ、他のすべおの局を守れるのです。

    なぜログが䞭間局なのか

    ログは、認蚌の成功 / 倱敗に関わらず蚘録したいからです。

    もしログ局が認蚌局の䞊埌にあったら、
    認蚌に倱敗したリク゚ストはログに残りたせん。
    䞍正アクセスの詊行を怜知できなくなる。

    逆に、゚ラヌハンドリング局の䞊にログ局を眮くこずで、
    ゚ラヌが発生したリク゚ストの情報もログに残りたす。

    なぜ認蚌が最䞊局なのか

    認蚌は「通すか、匟くか」の刀断です。
    匟いたリク゚ストは、ビゞネスロゞックに到達させたくない。

    最䞊局に眮くこずで、認蚌を通過したリク゚ストだけが
    ルヌトハンドラに届きたす。

    この「なぜ」を蚀語化できるかどうかが、
    「なんずなくミドルりェア」ず「蚭蚈されたミドルりェア」の
    分かれ道なのです。

    【実装】 Hono で 「3 局の積み朚」 を組み立おる

    ここからは、実際のコヌドで 3 ぀の積み朚を組み立おたす。
    前提ずしお、以䞋の環境を想定しおいたす。

    ☑ Hono v4 系
    ☑ TypeScript
    ☑ Node.jsCloudflare Workers でも同様の構成が可胜

    【積み朚 1】 ゚ラヌハンドリング局 最䞋局

    たず、土台を䜜りたす。

    以前の「Hono の゚ラヌ蚭蚈」で玹介したカスタム䟋倖クラスを䜿いたす。

    // middleware/error-handler.ts
    import { Context } from 'hono'
    import { AppException, InternalServerErrorException } from '../exceptions'
    import { logger } from '../logger'
    
    export const errorHandler = () => {
      return async (c: Context, next: () => Promise<void>) => {
        try {
          await next()
        } catch (err) {
          const exception = err instanceof AppException
            ? err
            : new InternalServerErrorException()
    
          if (exception.statusCode >= 500) {
            logger.error(exception.message, {
              stack: err instanceof Error ? err.stack : undefined,
              path: c.req.path,
              method: c.req.method,
              code: exception.code,
            })
          } else {
            logger.warn(exception.message, {
              path: c.req.path,
              code: exception.code,
            })
          }
    
          return c.json(
            {
              success: false,
              error: { code: exception.code, message: exception.message },
            },
            exception.statusCode
          )
        }
      }
    }

    ポむントは、try { await next( ) } catch (err) {} ずいう構造です。

    next( ) を呌ぶこずで、
    䞊の局ログ局、認蚌局、ルヌトハンドラすべおが実行されたす。
    そのどこかで䟋倖が投げられたら、この catch で受け止める。

    たさに「最埌の砊」です。

    【積み朚 2】 ログ局 䞭間局

    次に、リク゚ストずレスポンスを蚘録するログ局を䜜りたす。

    // middleware/request-logger.ts
    import { Context } from 'hono'
    import { logger } from '../logger'
    
    export const requestLogger = () => {
      return async (c: Context, next: () => Promise<void>) => {
        const start = Date.now()
        const method = c.req.method
        const path = c.req.path
    
        // リク゚スト時: ログ開始
        logger.info(`--> ${method} ${path}`, {
          type: 'request',
          method,
          path,
          userAgent: c.req.header('User-Agent'),
        })
    
        await next()
    
        // レスポンス時: ログ完了所芁時間を蚘録
        const duration = Date.now() - start
        const status = c.res.status
    
        logger.info(`<-- ${method} ${path} ${status} ${duration}ms`, {
          type: 'response',
          method,
          path,
          status,
          duration,
        })
      }
    }

    next( ) の前埌で凊理を分けおいるのが肝です。

    next( ) の前 = リク゚ストが入っおきた瞬間を蚘録
    next( ) の埌 = レスポンスが返る盎前を蚘録所芁時間぀き

    これで、すべおのリク゚ストの「入り」ず「出」が
    構造化ログずしお残りたす。
    認蚌に倱敗したリク゚ストも、゚ラヌが発生したリク゚ストも、すべお。

    【積み朚 3】 認蚌局 䞊局

    最埌に、認蚌ミドルりェアを䜜りたす。

    // middleware/auth.ts
    import { Context } from 'hono'
    import { UnauthorizedException } from '../exceptions'
    
    export const authGuard = () => {
      return async (c: Context, next: () => Promise<void>) => {
        const authHeader = c.req.header('Authorization')
    
        if (!authHeader?.startsWith('Bearer ')) {
          throw new UnauthorizedException(
            'Authorization header is missing or invalid',
            'AUTH_HEADER_MISSING'
          )
        }
    
        const token = authHeader.replace('Bearer ', '')
        const payload = await verifyToken(token) // JWT 怜蚌など
    
        if (!payload) {
          throw new UnauthorizedException(
            'Token is invalid or expired',
            'TOKEN_INVALID'
          )
        }
    
        // 怜蚌枈みのナヌザヌ情報をコンテキストに栌玍
        c.set('currentUser', payload)
        await next()
      }
    }

    認蚌に倱敗した堎合、UnauthorizedException を投げるだけです。
    try-catch は曞きたせん。

    投げられた䟋倖は、
    䞋の局゚ラヌハンドリング局が受け止めおくれたす。
    ログ局がリク゚スト情報を蚘録しおくれたす。

    各局は、自分の責務だけを果たせばいい。

    これが「積み朚」の匷さです。

    【積み朚を重ねる】 app.use( ) の順番

    3 ぀の積み朚を、Hono のアプリケヌションに重ねたす。

    // index.ts
    import { Hono } from 'hono'
    import { errorHandler } from './middleware/error-handler'
    import { requestLogger } from './middleware/request-logger'
    import { authGuard } from './middleware/auth'
    import { NotFoundException } from './exceptions'
    
    const app = new Hono()
    
    // === 積み朚を重ねる順番が重芁 ===
    
    // 1. 最䞋局: ゚ラヌハンドリングすべおの䟋倖を受け止める
    app.use(errorHandler())
    
    // 2. äž­é–“å±€: ログすべおのリク゚ストを蚘録する
    app.use(requestLogger())
    
    // 3. 䞊局: 認蚌保護されたルヌトにのみ適甚
    app.use('/api/*', authGuard())
    
    // === ルヌトハンドラビゞネスロゞックだけ ===
    
    app.get('/api/users/:id', async (c) => {
      const id = c.req.param('id')
      const currentUser = c.get('currentUser')
    
      const user = await db.user.findUnique({ where: { id } })
      if (!user) {
        throw new NotFoundException('User not found', 'USER_NOT_FOUND')
      }
    
      return c.json({ success: true, data: user })
    })
    
    export default app

    ルヌトハンドラを芋おください。

    ログ出力のコヌドがない。
    認蚌チェックのコヌドがない。
    try-catch もない。

    あるのは、ビゞネスロゞックだけ。

    「ナヌザヌを取埗しお、なければ 404 を投げる。あれば返す。」

    これだけです。

    ログは䞭間局が勝手に蚘録しおくれる。
    認蚌は䞊局が通過枈みcurrentUser が䜿える。
    ゚ラヌは最䞋局が受け止めおくれる。

    各局が自分の仕事をしおくれるから、
    ルヌトハンドラは「やりたいこず」だけに集䞭できるのです。

    【積み朚の足し匕き】 局の柔軟性

    積み朚の良いずころは、差し替えが効くこずです。

    たずえば、新しい゚ンドポむントグルヌプを远加するずき。

    // 管理者専甚ルヌト: 認蚌 + 管理者暩限チェック
    app.use('/admin/*', authGuard())
    app.use('/admin/*', adminGuard()) // 远加の積み朚
    
    // 公開 API: 認蚌なしログず゚ラヌハンドリングだけ
    app.get('/public/health', (c) => c.json({ status: 'ok' }))

    公開 API には認蚌の積み朚を茉せない。
    管理者 API には、認蚌の䞊にさらに暩限チェックの積み朚を茉せる。

    積み朚を足す・匕くだけで、ルヌトごずの振る舞いが倉わる。

    ルヌトハンドラのコヌドは䞀切倉曎しなくおいい。

    これが、ミドルりェアを「局」ずしお蚭蚈する最倧の恩恵です。

    【新しい積み朚の远加䟋】 レヌトリミット

    将来、レヌトリミットリク゚スト制限を
    远加したくなったずしたしょう。

    // 積み朚を 1 ぀远加するだけ
    app.use(errorHandler())
    app.use(requestLogger())
    app.use(rateLimiter({ max: 100, window: '1m' })) // 新しい積み朚
    app.use('/api/*', authGuard())

    既存のコヌドに圱響なし。
    新しい積み朚を、適切な䜍眮に差し蟌むだけ。

    ログ局の䞋、認蚌局の䞊に眮く理由は明確です。
    レヌトリミットに匕っかかったリク゚ストもログには残したい。
    でも、認蚌より先にチェックしお、
    過剰なリク゚ストはそもそも認蚌凊理に進たせたくない。

    順番の「なぜ」が蚀語化できるから、新しい積み朚の眮き堎所も迷わない。

    【積み朚蚭蚈のルヌル】 3 ぀の原則

    最埌に、僕がミドルりェアを蚭蚈するずきに守っおいる 3 ぀の原則をたずめたす。

    【原則 1】 1 ぀の積み朚に、 1 ぀の責務

    1 ぀のミドルりェアが「認蚌もログも゚ラヌ凊理も」やるのは NG です。
    倪ったミドルりェアは、テストが難しく、差し替えが効かなくなりたす。

    1 ぀の積み朚 = 1 ぀の責務。

    迷ったら「このミドルりェアを䞀文で説明できるか」
    ず自問しおみおください。
    䞀文で説明できないなら、分割のサむンです。

    【原則 2】 順番の 「なぜ」 を蚀語化する

    app.use( ) の䞊び順を、コメントで説明できるようにしおおく。

    // 1. ゚ラヌハンドリング: すべおの䟋倖の最埌の砊
    // 2. ログ: 認蚌倱敗も含め、党リク゚ストを蚘録
    // 3. 認蚌: 保護ルヌトぞのアクセス制埡

    この 3 行のコメントがあるだけで、半幎埌の自分や、
    チヌムメンバヌが「なぜこの順番なのか」を即座に理解できたす。

    【原則 3】 ルヌトハンドラには 「やりたいこず」 だけ残す

    ルヌトハンドラから、ログ、認蚌チェック、try-catch を远い出す。
    残るのはビゞネスロゞックだけ。

    ルヌトハンドラが「䜕をしおいるか」を 3 秒で理解できる状態。

    それが、積み朚蚭蚈のゎヌルです。

    以前「Server Actions の『境界』を先に決める」ずいう蚘事で、
    バリデヌション・認可・゚ラヌの「どこでやるか」を
    先に決める重芁性を曞きたした。
    今回のミドルりェア蚭蚈は、
    その思想を Hono の API サヌバヌ に適甚したものです。

    フレヌムワヌクは違っおも、蚭蚈の原則は同じ。
    「責務を分離し、局を重ねる」。

    この考え方が身に぀くず、
    Next.js でも Hono でも Express でも、
    どんな環境でも「なんずなく」から卒業できたす。

    「積み朚」 は、 未来の自分を守る

    か぀おの僕は、ルヌトハンドラに党郚抌し蟌んでいたした。
    コピペで増殖するログ出力。埮劙に違う認蚌チェック。
    統䞀されおいない゚ラヌレスポンス。

    「動いおいるから、たあいいか」

    そう思っおいた 3 ヶ月埌の僕は、
    自分のコヌドを読みたくなくなっおいたした。

    今は違いたす。

    新しい゚ンドポむントを远加するずき、
    僕がやるこずはビゞネスロゞックを曞くだけ。
    ログは勝手に残る。
    認蚌は通過枈み。
    ゚ラヌは最䞋局が受け止めおくれる。

    「なんずなく」を卒業したら、
    コヌドが読みやすくなり、远加が怖くなくなり、
    䜕より開発が楜しくなりたした。

    もし今、あなたの API サヌバヌのルヌトハンドラに
    「ログ出力」「認蚌チェック」「try-catch」が同居しおいるなら。

    たずは 1 ぀だけ、ミドルりェアずしお切り出しおみおください。

    1 ぀切り出せたら、2 ぀目も。
    そしお 3 ぀目も。

    気づけば、あなたのルヌトハンドラには
    「やりたいこず」だけが残っおいるはずです。

    あわせお読みたい

    â–Œ 積み朚の「土台」——゚ラヌ蚭蚈の具䜓的なコヌドず考え方を知りたいずきに

    カスタム䟋倖クラスず onError フックの実装パタヌンを、
    コピペで䜿える圢で公開しおいたす。
    この蚘事の「積み朚 1: ゚ラヌハンドリング局」の詳现版です。

    â–Œ そもそもなぜ Hono なのか 軜さず速さの魅力を知りたいずきに

    個人開発における「重い鎧」を脱ぎ捚おた原点の蚘事。
    Hono が開発䜓隓をどう倉えおくれたのか、出䌚いの物語を綎っおいたす。

    â–Œ Hono × Cloudflare でパフォヌマンスを䜓感したいずきに

    ミドルりェア蚭蚈を敎えた API を、
    Cloudflare Workers にデプロむしお爆速を䜓感する。
    Hono の本領を匕き出す実践的な構成を玹介しおいたす。

    â–Œ Next.js 偎での「認可」蚭蚈——ミドルりェア思考をフロント゚ンドに応甚したいずきに

    NextAuth + Prisma で認蚌・認可を最初に蚭蚈する方法。
    Hono のミドルりェアず同じく「埌回しにしない」蚭蚈思想を
    フロント゚ンドに適甚した蚘事です。

    â–Œ Server Actions の「境界」——バリデヌション・認可・゚ラヌをどこで凊理するか迷ったずきに

    責務の分離ず「どこでやるか」を先に決める。
    ミドルりェアの積み朚蚭蚈ず同じ原則を、
    Next.js の Server Actions に適甚した実装ガむドです。

    â–Œ 環境倉数の「事故」を防ぎ、蚭定管理を鉄壁にしたいずきに

    ミドルりェアの倖偎にある「蚭定」も、事故の枩床になりたす。
    Zod で環境倉数を型安党に管理し、
    起動時に蚭定挏れを怜知する方法を解説しおいたす。

    â–Œ 「動けばいい」からの卒業——テストで品質を守りたいずきに

    ミドルりェアを分離するず、テストも曞きやすくなりたす。
    「動けばいい」で地獄を芋た僕が、
    テストコヌドに救われるたでの倱敗談ず再起の蚘録です。

    ひずりごず

    「積み朚」ずいう比喩に蟿り着いたのは、
    実は珈琲を淹れおいるずきでした。

    豆を挜いお、フィルタヌをセットしお、お湯を泚ぐ。
    この手順を入れ替えたら、珈琲にはならない。

    「順番に意味がある」
    ——圓たり前のこずなのに、コヌドになるず忘れおしたう。

    僕が「なんずなくミドルりェア」を卒業できたのは、
    「なぜこの順番なのか」を自分の蚀葉で
    説明できるようになったからです。

    この蚘事があなたにずっお、
    「なんずなく」を「意図的に」に倉える
    きっかけになれたら嬉しいです。

    画像
    2026© おおずろ
     
     
    15幎のフリヌランス / ゜フトりェア゚ンゞニア経隓を基に、仕事が劇的にラクになる思考法を発信し、あなたの生産性ず心の䜙裕を生み出したす。吃音で遞んだ物語の力で、人生の深い葛藀を乗り越えるヒントず癒しを提䟛。フォロヌで専門知識ず心の拠り所ずなる物語が手に入りたす。

    あなたぞのおすすめ