🔥

Next.js の Middleware で Hono を使う

2024/05/01に公開

株式会社 CoeFont でフロントエンドエンジニアをしている uzimaru です。
Next.js の Middleware で Hono を使ってみたのでそれについて記事にまとめます。

モチベーション

Next.js の Middleware はアプリケーションに1つだけしか設定出来ず、どの path で実行するかの設定も config を使って正規表現や Header, Cookie を指定して設定するかリクエストの pathname を見て処理を分岐する必要があります。

シンプルな実装のみなら良いのですが、path によってログインしているか確認したい、いくつかの処理を Middleware で適応したい、というように要件が複雑になると管理が大変になっていくと思います。

そこで、Hono のようなシンプルなフレームワークを Middleware で動かして実装をシンプルにしようというのがモチベーションです。

Hono とは

公式ドキュメントより

Hono - [炎] means flame🔥 in Japanese - is a small, simple, and ultrafast web framework for the Edges. It works on any JavaScript runtime: Cloudflare Workers, Fastly Compute, Deno, Bun, Vercel, Netlify, AWS Lambda, Lambda@Edge, and Node.js.

いろいろなランタイムで動く WebFramework です。
より詳しくは、作者様の書いたこちらの記事を参照してください。

Middleware で Hono を動かしてみる

この Hono ですが Web標準API のみを利用して動いています。
また、Next.js の Middleware が受け取るシグネチャを見ると

type NextMiddleware = (
  request: NextRequest,
  event: NextFetchEvent
) => NextMiddlewareResult | Promise<NextMiddlewareResult>;

という形になっています。
この NextRequest ですが Request を継承した型になっています。Web標準ですね。
返り値の NextMiddlewareResult

export type NextMiddlewareResult = NextResponse | Response | null | undefined | void;

という型になっています。こちらもWeb標準ですね。

そのため、普通に Hono を使うだけでも十分動くということが分かったと思います。
最低限のコードは以下のようになります

import { Hono } from  'hono'
import { NextRequest } from 'next/server'

const app = new Hono()

app.all('*', (ctx) => {
    return ctx.body(null)
})

export const middleware = (req: NextRequest) => {
    return app.fetch(req)
}

ただし、このコードでは想定している挙動はしてくれません。
見て分かるように、app.all で空のレスポンスを返しているためページにアクセスしても空のレスポンスが返ってきてしまいます。

Middleware で Hono を使う

このままだとまともに使えないので使えるようにします。

NextResponse のドキュメントを見ると、AppRouterに処理を返すには NextResponse.next を使えば良さそうです。

import { Hono } from  'hono'
import { NextRequest } from 'next/server'

const app = new Hono()

app.all('*', (ctx) => {
    return NextResponse.next()
})

export const middleware = (req: NextRequest) => {
    return app.fetch(req)
}

これで AppRouter に実装したページが表示されます。

処理を Middleware でまとめる

Middleware で実現した処理を Hono の Middleware として実装してまとめます。ちょっとややこしいですね。

今回は例として accept-language の中身に応じて LOCALE という cookie を設定する処理を Middleware にします。

import { MiddlewareHandler } from 'hono'
import { getCookie } from 'hono/cookie'
import { NextRequest } from 'next/server'

const i18nMiddleware: MiddlewareHandler = async (ctx, next) => {
  let locale =
    ctx.req.header('accept-language')?.split(',')[0]?.split('-')[0]

  if (locale !== 'en') {
    locale = 'ja'
  }

  if (!getCookie(ctx, 'LOCALE')) {
    const req = ctx.req.raw as NextRequest
    req.cookies.set('LOCALE', locale)
  }

  return next()
}

書き方は一般的な Hono の Middleware と同じですが、いくつかポイントがあります。

ctx.req.raw について

Hono では ctx.reqHonoRequest という型の値が入っています。
この値は、Request を Wrap したものなので元の Request を参照するには ctx.req.raw を使って参照出来ます。
ここで、「元の Request」が何だったのかを思い出しましょう。そう、NextRequest ですね。
そういう訳で ctx.req.rawNextRequest に変えています。

cookie に付いてですが、取得に関しては hono/cookie にある getCookie で問題ないと思います。
問題は、設定の方です。
今回の設定には Request の方に cookie を設定しています。
これは、NextResponse.next() を使って AppRouter に引き継いだときの Cookie に反映させるためです。
Middleware をアプリケーションの前段の処理として扱うときはこの Request に cookie を設定する方を使いましょう。
後続のアプリケーションで使わない cookie の設定には hono/cookie にある setCookie でいいと思います。

Hono の Middleware を設定する

作成した Hono の Middleware を設定します。
今回は全部の path に対して有効にしたいので以下のように書きます。

const app = new Hono()

app.use(i18nMiddleware)

しかし、この状態だと LOCALE が cookie に反映されません。
問題は、app.all にあります。こちらに引き継ぎたい Request についてを設定していないので反映されていませんでした。

app.all('*', ctx => {
    const req = ctx.req.raw as NextRequest
    return NextResponse.next({
        request: req
    })
})

これで Middleware で設定した cookie が反映されるようになりました。

Middleware の定義部分について

最初に書いたコードでは

export const middleware = (req: NextRequest) => {
    return app.fetch(req)
}

のように書いていましたが、実は hono/vercel にある handle 関数を使えます。
これは Hono を Vercel で動かすときの Adaptor なのですが、middleware のシグネチャをよく見ると Vercel の Runtime が受け取る型と同じです(というより、Vercel で動くように Next が作られているのほうが正しそう)
そのため、hono/vercel が使えます。

使うとこんな感じ

import { handle } from 'hono/vercel'

export const middleware = handle(app)

シンプルになりましたね。

まとめ

Hono が Web標準API を使った実装になってくれているおかげで Next.js の Middleware で簡単に使うことができました。
基本的にはそのまま使えるのですが、Response 周りや cookie 周りがやや特殊だったのでそこだけ気をつける必要がありそうです。

Hono を使って書き換えたおかげで Middleware の見通しが良くなったのが良かったです。
Next.js の Middleware の書きにくさを感じている人は是非試してみてください。

CoeFont

Discussion