🐢

特定のルートでのみ指定したミドルウェアを実行できるようにする

2024/11/10に公開

これは何

タイトルの通りです。
Next.jsのミドルウェアは、matcherでミドルウェアの一致/不一致を設定できることに加えて、条件分岐で適用/不適用を制御できます。

この仕組みを応用し、route1ではmiddleware1, route2ではmiddleware2,3 のような形で実行できるようにしました。

ユースケース

SaaSで、ライセンスを購入すると特定のルーティング配下でのみ有効になる機能を提供しているとします。ライセンスが切れた時にユーザーが機能URLに直接アクセスしたら、ライセンスの購入画面へリダイレクトして再購入を訴求する画面を表示します。

やったこと

参考記事に従い、対象ファイルを記載しました:

作成したファイル

middlewares/types.ts
import { NextMiddleware } from "next/server";

export type MiddlewareFactory = (middleware: NextMiddleware) => NextMiddleware;
middlewares/stackMiddleware.ts
import { NextMiddleware, NextResponse } from "next/server";
import { MiddlewareFactory } from "./types";

export function stackMiddleware(
  functions: MiddlewareFactory[] = [],
  index = 0
): NextMiddleware {
  const current = functions[index];

  if (current) {
    const next = stackMiddleware(functions, index + 1);
    return current(next);
  } else {
    // currentがfalseになると、上記のconst next にNextResponse.next が代入される
    return () => NextResponse.next();
  }
}

ミドルウェアは玉ねぎのように、現在のミドルウェアが次のミドルウェアを包むような形で実行されます。上記のコードでミドルウェアが1,2,3の順番でコールされた場合、middleware1(middleware2(middleware3(NextResponse.next())))のようにミドルウェアのスタックが積まれて、最後にレスポンスが実行されます。

次に、特定のルートに対応するミドルウェアを作ります。

middlewares/redirectHoge.ts
import {
  NextMiddleware,
  NextRequest,
  NextResponse,
  NextFetchEvent,
} from "next/server";
import { MiddlewareFactory } from "./types";

export const redirectHoge: MiddlewareFactory = (middleware: NextMiddleware) => {
  return async (request: NextRequest, _event: NextFetchEvent) => {
    if (request.nextUrl.searchParams.has("redirect")) {
      return NextResponse.redirect(new URL("/dashboard", request.url));
    }

    return middleware(request, _event);
  };
};

NextMiddlewareを渡して、リクエストオブジェクトを含むNextMiddlewareを返却する関数を作ります。middleware()asyncで定義するので、作成する関数も同様に非同期にします。

最後に、親となるミドルウェア定義ファイルを作成します:

middleware.ts
import { NextFetchEvent, NextResponse, type NextRequest } from "next/server";
import { stackMiddleware } from "./middlewares/stackMiddleware";
import { redirectHoge } from "./middlewares/redirectHoge";
import { MiddlewareFactory } from "./middlewares/types";

// mapping route and middleware
const routeMiddlewares: { [key: string]: MiddlewareFactory[] } = {
  "/hoge": [redirectHoge],
};

export async function middleware(request: NextRequest, event: NextFetchEvent) {
  const response = NextResponse.next();
  const path = request.nextUrl.pathname;

  for (const route in routeMiddlewares) {
    if (path.startsWith(route)) {
      const middlewares = routeMiddlewares[route];
      const stackedMiddleware = stackMiddleware(middlewares);
      return stackedMiddleware(request, event);
    }
  }
  return response;
}

export const config = {
  matcher: [
    /*
     * Match all request paths except for the ones starting with:
     * - _next/static (static files)
     * - _next/image (image optimization files)
     * - favicon.ico (favicon file)
     */
    "/((?!_next/static|_next/image|favicon.ico|.*\\.(?:svg|png|jpg|jpeg|gif|webp)$).*)",
  ],
};

routeMiddlewaresに定義したルートに遷移した場合に、対応するミドルウェアであるredirectHogeを実行します。挙動としては、/hogeに遷移した場合にクエリパラメータにredirectが定義されていれば/dashboardにリダイレクトするになります。

以上です

Discussion