Next.js の Middleware で Hono を使う
株式会社 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.req
に HonoRequest
という型の値が入っています。
この値は、Request
を Wrap したものなので元の Request
を参照するには ctx.req.raw
を使って参照出来ます。
ここで、「元の Request
」が何だったのかを思い出しましょう。そう、NextRequest
ですね。
そういう訳で ctx.req.raw
を NextRequest
に変えています。
cookie の取り扱い
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 の書きにくさを感じている人は是非試してみてください。
Discussion