LoginSignup
41
22

Next.jsでの初心者向け Middleware.ts入門 (v13.1.0) + 公式マニュアル 解説 + 複数のMiddlewareの実装方法

Last updated at Posted at 2023-09-02

Next.js Middleware ドキュメント

Routing: Middleware | Next.js

※Middleware v13.1.0 時点のドキュメントを参考にしています。

対象

Middlewareって何?
どんな事ができるの?
どんな機能があるの?
と疑問を持っている人。

切り貼りコピペで使っていた人(=自分)向け。

Middleware について 初心者向けなことや Next.js 公式ドキュメントを調査していきます。

Middlewareを5歳でも理解できるように説明をしてください。 (by GitHub Copilot chat)

ミドルウェア(Middleware)は、ちょうど「中間の仕事をする人」のようなものです。

例えば、あなたがレストランで食事をするとき、ウェイターがミドルウェアのような役割を果たします。あなた(クライアント)が注文をすると、ウェイター(ミドルウェア)はその注文を受け取り、厨房(サーバー)に伝えます。そして、厨房が料理(データ)を作ったら、ウェイターがそれをあなたに運びます。

このように、ミドルウェアはクライアントとサーバーの間で働き、リクエストとレスポンスを適切に処理します。ミドルウェアは、リクエストがサーバーに到達する前に何か特別な処理をしたり(例えば、セキュリティチェックを行ったり)、レスポンスをクライアントに送る前に何かをしたり(例えば、データを整形したり)します。

Middlewareの主な仕事

認証処理
画像処理
リダイレクト
リライト
ログ
国際化
キャッシュ
セキュリティ (不正なアクセスを弾く)
その他

Middleware の置く場所

Next.jsは規約で middleware.ts は1つだけです。

middleware.ts の置く場所
root
app/
pages/
src/

※重要: Next.jsのインストール時に srcフォルダを作った場合は、srcフォルダの下にmiddleware.tsファイルを置いてください。

src/middleware.ts

Middlewareの拡張子

middleware.ts
middleware.js

この拡張子が使えます。

Middlewareの仕事の流れ

Middleware とは、Next.js アプリケーションにリクエストが到達する前に実行されるコードの層です。

通常、Next.js はリクエストを受け取り、ルーティングに基づいて適切なページまたは API エンドポイントに処理を渡します。

しかし、Middleware を使用すると、Next.jsのリクエスト処理の前に、リクエストのチェックを行います。

最初に matcherがリクエストを処理します。

matcherの仕事

matcherはブラウザからのリクエストを絞り込む機能です。

まず最初にユーザーからのリクエストを Middleware でキャッチして、ユーザーからのリクエストのパスが、

  • matcherにマッチした場合

Middleware が実行されます。

  • matcherにマッチしなかった場合

ユーザーのリクエストは Middleware を素通りして Next.js に渡されます。

Middleware が実行された場合。

Middleware に書いたコードが実行されリクエストが処理されます。

Middleware 関数は、2 つの引数を受け取ります。

request: NextRequest オブジェクト。このオブジェクトには、リクエストに関する情報が含まれています。
response: NextResponse オブジェクト。このオブジェクトを使用して、リクエストへの応答を制御できます。

その処理したリクエストを、Next.jsに渡すか、弾くかをします。

正常なリクエストならば、Next.jsに渡します。

Middleware が弾いた場合。

弾いた場合はレスポンスを作成する作業を Middleware が行います。

Middleware はレスポンスを作りますが、その中身は当然 空っぽです。
通常、中身を作る作業はNext.jsで行うのですから、Middleware にそんな能力はありませんので、返す形だけを作ります。

実際のコードはこのようになります。

middleware.ts

const res = NextResponse.next()

このように レスポンスを作ります。 しかしこの中身はレスポンスの形だけが決まっているだけの空っぽな const変数 です。

※const 変数は 再代入出来ないという規則がありますが、配列やオブジェクトの場合、中身を変更することが可能です。

つまり この const の宣言は 箱の形だけは決まって変えられないけど、中身は空っぽな箱で色々詰められる ということになります。

この箱は ユーザーにもNext.jsにも渡すことが出来ます。中身は単なる空っぽの箱ですが共通の規格なのでブラウザもNext.jsも利用することが可能だからです。

ユーザーからのリクエストの毒見役というか、検閲役というか、そんな役割が Middleware に求められます。

Middleware は Next.jsとは別に独立したバックエンドの一種と考えれば理解しやすいと思います。

Middlewareの分割

Next.jsはmiddleware.tsは1ファイルのみという規約があります。

なので、JavaScriptの分割テクニックを使って、複数のMiddlewareファイルに分割させます。

複数に分けて書くほうが書きやすいですし、読む方も理解しやすく、メンテナンスもしやすくなります。

サンプルを見てみる

Next.js には Middleware の↓公式サンプルがあります。

middleware

↑このリポジトリの ↓ middleware.ts を見てみます。

middleware.ts
import { NextRequest, NextResponse } from 'next/server'

export function middleware(request: NextRequest) {
  if (request.nextUrl.pathname === '/about') {
    return NextResponse.redirect(new URL('/redirected', request.url))
  }
  if (request.nextUrl.pathname === '/another') {
    return NextResponse.rewrite(new URL('/rewrite', request.url))
  }
  return NextResponse.next()    <<< 到達不可能
}

export const config = {
  matcher: ['/about/:path*', '/another/:path*'],
}

文法は2つのシンプルなif文と、何もなかったら 空のレスポンスを 返す return文 というごく単純なコードです。

config に関しては後述

NextRequest と NextResponse は単なる型です。

3行目のコード
request.nextUrl.pathname === '/about'

trueならばリダイレクトを行います。

7行目のコード
request.nextUrl.pathname === '/another'

trueならばリライトを行います。

redirectと rewrite という2つの処理の違い

redirect
クライアントを別のURLに転送することを目的としています。

rewrite
クライアントがアクセスしたURLを別のURLに書き換えることを目的としています。

const config

この const config は Middleware が自動的に読み込みます。

このコードでは matcher が設定してあり、ユーザーからのリクエストのパスがこの matcher にマッチした場合に、このMiddlewareが実行されます。

matcherが設定されているため、マッチしないユーザーのリクエストはこのミドルウェアを通過しません。その結果、最後のReturn文は実質的に到達することはありません。

Middleware の実行パス

マッチャーにより引っかかったリクエストに対して実行します。
リクエストを解析したパスを 条件分岐で分けます。

マッチャーには正規表現を使用できます。

マッチャー
サンプルコードより

/about/:path*
/another/:path*

/about/や/another/
ユーザーのリクエストがこのパスにマッチした場合に、この Middleware が実行されます。

:path*
コロンは名前付きパラメータを表します。

プレースホルダー
:[文字列]

プレースホルダーとは、あらかじめ決まっていない値を表すために、一時的に置かれる場所のことを指します。プログラミングにおいては、プレースホルダーは、変数や引数、フォーマット文字列などの形で使用されます。
Webアプリケーションにおいては、URLのパスにプレースホルダーを使用することで、動的なページを生成することができます。
プログラミングにおいては、プレースホルダーは、可読性や保守性を高めるためにも重要な役割を果たします。

matcher:path*はプレースホルダーの1つです。
/about/:path*というパスは、 /aboutの後に/で区切られた任意の文字列を含むパスにマッチします。
例えば、 /about/foo/about/bar/bazにマッチします。

このように、 プレースホルダー を使用することで、動的なパスを扱うことができます。matcherには、他にもプレースホルダーがあります。例えば、 :idは数値のIDを表し、 :slugは文字列のスラッグを表します。

* は正規表現で、0回以上の繰り返しを表します。

複数のURLパターンをマッチさせることができます。

matcher: ['/about/:path*', '/dashboard/:path*'],

このように配列に入れることで、複数のパスのパターンを設定できます。

ここまでの説明で、
マッチャーで複数のパスを設定することができること、
そして、その複数のそれぞれ個別のパスに対して、それぞれ個別の処理を行いたい場合は、ifなどの条件式を使って、それぞれのパスに対して処理を行うことになります。

/about/:path* にマッチする例

/about/ <<0回以上の繰り返しなので、このパスにもマッチします。
/about/foo
/about/bar/baz

startsWith()というメソッド

request.nextUrl.pathname

↑この pathname には startsWith() というメソッドがあります。

使用例
request.nextUrl.pathname.startsWith('/about')

/aboutにマッチするパスがあった場合に、この コード が実行されます。

NextResponse

NextResponseのコードを遡ってみていくと、(VSCode の F12ボタン)

response.d.ts
declare const INTERNALS: unique symbol;
export declare class NextResponse<Body = unknown> extends Response {
    [INTERNALS]: {
        cookies: ResponseCookies;
        url?: NextURL;
        body?: Body;
    };
    constructor(body?: BodyInit | null, init?: ResponseInit);
    get cookies(): ResponseCookies;
    static json<JsonBody>(body: JsonBody, init?: ResponseInit): NextResponse<JsonBody>;
    static redirect(url: string | NextURL | URL, init?: number | ResponseInit): NextResponse<unknown>;
    static rewrite(destination: string | NextURL | URL, init?: MiddlewareResponseInit): NextResponse<unknown>;
    static next(init?: MiddlewareResponseInit): NextResponse<unknown>;
}

こんな感じです、ジェネリック型というものが使われています。
そのジェネリック型はBodyでありデフォルト値はunknownです。

つまり NextResponseは基本の値は全て決まっていてあとから
開発者がBodyにデータを入れる形になっているということになります。

開発者はこのBodyにMiddleware で加工したデータを入れていきます。

[INTERNALS]は配列ではなくて、2015年に追加されたシンボルです。

※シンボルとは、プリミティブ型の一種で、文字列と似ていますが、文字列とは異なり、同じ値を持つシンボルは必ず異なるものとして扱われます。
unique symbolと宣言されているので、このシンボルは一意であることが保証されています。

NextResponseの解説

declare const INTERNALS: unique symbol;

constキーワードを使用して、 INTERNALSという名前の定数を宣言しています。unique symbolは、一意のシンボルを表す型です。

export declare class NextResponse<Body = unknown> extends Response {

NextResponseというクラスを宣言しています。<Body = unknown>は、ジェネリック型の宣言で、 Bodyという型引数を持ち、デフォルト値としてunknownを指定しています。extends Responseは、 NextResponseResponseクラスを継承することを示しています。

[INTERNALS]: {
    cookies: ResponseCookies;
    url?: NextURL;
    body?: Body;
};

[INTERNALS]というプロパティに、 cookiesurlbodyというプロパティを持つオブジェクトを定義しています。cookiesは、 ResponseCookies型、 urlは、 NextURL型、 bodyは、 Body型です。

constructor(body?: BodyInit | null, init?: ResponseInit);

constructorメソッドを定義しています。bodyinitという2つの引数を持ちます。bodyは、 BodyInit型またはnullinitは、 ResponseInit型です。

get cookies(): ResponseCookies;

cookiesというgetterメソッドを定義しています。戻り値の型は、 ResponseCookiesです。

static json<JsonBody>(body: JsonBody, init?: ResponseInit): NextResponse<JsonBody>;

jsonという静的メソッドを定義しています。<JsonBody>は、ジェネリック型の宣言で、 JsonBodyという型引数を持ちます。bodyinitという2つの引数を持ちます。戻り値の型は、 NextResponse<JsonBody>です。

static redirect(url: string | NextURL | URL, init?: number | ResponseInit): NextResponse<unknown>;

redirectという静的メソッドを定義しています。urlinitという2つの引数を持ちます。urlは、 string型、 NextURL型、 URL型のいずれかを受け取ることができます。initは、 number型またはResponseInit型です。戻り値の型は、 NextResponse<unknown>です。

static rewrite(destination: string | NextURL | URL, init?: MiddlewareResponseInit): NextResponse<unknown>;

rewriteという静的メソッドを定義しています。destinationinitという2つの引数を持ちます。destinationは、 string型、 NextURL型、 URL型のいずれかを受け取ることができます。initは、 MiddlewareResponseInit型です。戻り値の型は、 NextResponse<unknown>です。

static next(init?: MiddlewareResponseInit): NextResponse<unknown>;

nextという静的メソッドを定義しています。initという1つの引数を持ちます。initは、 MiddlewareResponseInit型です。戻り値の型は、 NextResponse<unknown>です。

以上が、 NextResponseクラスの定義に関する解説です。

このクラスは、Next.jsのMiddlewareで使用されるレスポンスオブジェクトを拡張したものであり、 redirectrewriteなどのメソッドを提供しています。

以上がNextResponseの簡易な説明です。

2つ目の Middleware

Middleware で1つのチェックをする方法は学びました、次は複数のチェックをする場合、Middleware で実現するのはどうすればいいかと言う問題が出てきましたので調べたいと思います。

Middlewareで先程2つのPathをチェックしました。そしてそれぞれマッチした場合に、リダイレクトとリライトを行いました。
しかし、Middlewareで他の処理を行いたい場合もあります。その場合はどうすればいいのでしょうか?

↓Middlewareで行いたい処理。

Path <<Pathをチェックした例を前項目で解説しました。
認証 <<他にもMiddlewareで行いたい処理があります。
log
i18n

定義

Middlewareの1つの仕事

matcherで指定された1つのパスにマッチするリクエストに対して、ミドルウェア関数を実行する。

例えば、国際化 i18n でマッチする matcher の設定と、認証にマッチする matcher の設定をそれぞれ1つと数えます。

Middleware を調べた時に躓いたこと

Middlewareを調べていて
最初に躓くのが Middleware 自身の理解とその立ち位置

自分は、フロントエンド、バックエンド、DBに続く第4番目の役割、立ち位置と考えています。

認証は扉をつくって鍵をかけ、開くという役割。
DBはRLS(Row Level Security)等のポリシーで鍵をかけますが、

Middleware は 門番という役割でしょうか。
全通信のチェックをします。

次が見知らぬ文法でした。
オブジェクトに配列記号の[]を使ってシンボルを作るとうことを初めて勉強しました。

そして第三の壁が 2つ目のMiddleware を追加する時どうすればいいのかという問題でした。

これから、2つ目の Middleware を 実装する方法を調べていきます。

複数の処理を middleware.ts 1ファイルで行う?

まずは0個の処理、つまり受け流すだけの処理を書きます。
そこを基礎として1つづ増やしていく方法を考えます。

でもその前にすこし問題です。

問題1

問題1

Next.js 13 App router の middlewareに console.log を書いた場合、どこに出力されるでしょうか?

シンキングタイム

まず、middlewareの役割を思い出してみましょう。middlewareは、ブラウザからのリクエストをサーバーで処理する前に、様々な処理を行うためのものです。つまり、サーバーに到達する前にmiddlewareが処理を実行する必要があります。

次に、 console.logがどこに出力されるかを考えてみましょう。console.logは、ブラウザのコンソール画面に出力されます。しかし、middlewareはサーバーサイドで実行されるため、ブラウザのコンソール画面には出力されません。

つまり、middlewareにconsole.logを書いた場合、サーバーサイドのコンソール画面に出力されます。

middlewareの立ち位置をもう一度考えてください。

middlewareは基本的にブラウザからのリクエストをサーバーで処理する前に、様々な処理を行う検問所みたいな役割を持ちます。

つまり、サーバー到達前にmiddlewareが処理を実行する必要があります。

サーバーに到達する前に処理を実行するのならば console.logはブラウザ側のコンソール画面に出るでしょうか?

どこに出力されるのか、少し考えてみてください。

回答1

回答1

答えはサーバー側です。
Next.js のmiddlewareは単なる Next.js 機能の一つでしかありません。

Next.js 13 App router の全てはサーバーサイドで処理をするようになりました。

では、middlewareに'use client'を付けたらどこで実行されるでしょうか?

これも答えはサーバー側です。

'use client' ディレクティブを付けても付けなくても
middlewareはサーバー側に console.logを出力します。
'use server' ディレクティブを付けても同じです。

App routerやディレクティブなどに関係なくNext.jsではmiddlewareをサーバーサイドの処理として扱っています。

なのでどのように設定しても答えはサーバー側になります。

ただユーザーのリクエストの流れは Next.jsとmiddlewareはそれぞれ独立しています。

問題2

問題2

↓このように matcher を消したらmiddlewareはどうなるでしょうか?

export const config = {};

  • A すべてのパスをmiddlewareがチェックをします。
  • B すべてのパスはmiddlewareを素通りします。
  • C Next.jsのアプリケーションの設定に依るので不定である。

シンキングタイム

何も設定していないのならmiddlewareの機能は働かないのか?
もし、すべてmiddlewareを通るのなら単に負荷がかかるだけではないのか?

回答2

答えはすべてのパスを middleware がチェックをします。
matcherというフィルターを掛けて、マッチしたパスだけをmiddlewareがチェックをします。
そのフィルターが無いのですべてのユーザーからのリクエストをmiddlewareがチェックをします。
middleware.ts を設定した以上当然の処理の流れです。

問題3

面倒になったので ここからは駆け足で 解答も書いていきます。

Q ↓このように matcherを消して、リロードしたらどうなる?

export const config = {};

  • A キャッシュが効いているのでmiddlewareは働かない。
  • B すべてのパスをmiddlewareがチェックをします。 <<<正解

Q matcherは設定してあるが、配列が空の場合は?

middleware.ts
export const config = {
  matcher: [],
};

  • A matcherを設定していない時と同じになる。

  • B ↓root "/" が設定してある場合と同じになる。

export const config = {
  matcher: ["/"],
};

それにキャッシュは効かないのでconsole.logはリロード毎に表示される。

  • C root "/" が設定してある場合と同じになる。 <<<正解
    しかし、キャッシュが効いている?ので middlewareは動かない。
    (console.logで表示されない。)
    ※ブラウザをリロードしても console.log は表示されます。

Q matcher の設定を空にする

export const config = {
  matcher: [""],
};

  • A matcherを設定していない時と同じになる。

  • B root "/" が設定してある場合と同じになる。

  • C root "/" が設定してある場合と同じになる。
    それにキャッシュが効いているので middlewareは動かない。
    (console.logで表示されない。)

  • D Error: Invalid middleware found と報告が表示され、エラーになり(開発)サーバーが落ちる。 <<<正解

Next.js issue#57388

https://github.com/vercel/next.js/issues/57388

Middlewareの図解

  • 上が普通のブラウザからWebアプリへのデータの流れ
  • 下が、ユーザーのリクエストの流れです。
    図のようにMiddlewareが門番のようにユーザーのリクエストをチェックしています。

Middleware-04.png

実際に動かしてみます。

middleware.ts
import { NextRequest, NextResponse } from "next/server";

export function middleware(request: NextRequest) {
  console.log(
    "middleware.ts: request.nextUrl.pathname",
    request.nextUrl.pathname
  );

  return NextResponse.next();
}

export const config = {};

↑このコードはmiddlewareに何の処理もさせていません、console.logを表示させているだけです。

ローカルサーバー(npm run dev)を立ち上げるだけで、

middleware.ts: request.nextUrl.pathname /
middleware.ts: request.nextUrl.pathname /_next/static/webpack/934f8f934d3a6f9d.webpack.hot-update.json
middleware.ts: request.nextUrl.pathname /
middleware.ts: request.nextUrl.pathname /_next/static/chunks/webpack.js
middleware.ts: request.nextUrl.pathname /_next/static/chunks/react-refresh.js
middleware.ts: request.nextUrl.pathname /_next/static/chunks/main.js
middleware.ts: request.nextUrl.pathname /_next/static/chunks/pages/_app.js
middleware.ts: request.nextUrl.pathname /_next/static/development/_buildManifest.js
middleware.ts: request.nextUrl.pathname /_next/static/development/_ssgManifest.js
middleware.ts: request.nextUrl.pathname /_next/static/chunks/pages/index.js
middleware.ts: request.nextUrl.pathname /_next/static/development/_devMiddlewareManifest.json
middleware.ts: request.nextUrl.pathname /_next/static/development/_devPagesManifest.json
middleware.ts: request.nextUrl.pathname /favicon.ico

このようにmiddlewareを通ってきたことがterminalに出力されています。

では、これからの作業を見やすくするために、
config の matcher に "/" ルートだけを設定します。
(後で自分の好きなように設定し直してください。)

middleware.ts

・・・

export const config = {
  matcher: ["/"],
};

ブラウザをリロードすると

middleware.ts: request.nextUrl.pathname /

middleware はルートだけをチェックしたことがわかります。

middleware はconst config を自動的に読み込まれていることがわかります。
そして主に config には マッチャーが設定されています。

1つ目の処理の追加

Next.js 公式サンプル middleware

next.js/examples/middleware at canary · vercel/next.js

middlewareは↓このようになっています。

middleware.ts
import { NextRequest, NextResponse } from 'next/server'

export function middleware(request: NextRequest) {
  if (request.nextUrl.pathname === '/about') {
    return NextResponse.redirect(new URL('/redirected', request.url))
  }
  if (request.nextUrl.pathname === '/another') {
    return NextResponse.rewrite(new URL('/rewrite', request.url))
  }
  return NextResponse.next()
}

export const config = {
  matcher: ['/about/:path*', '/another/:path*'],
}

このサンプルを動かすと
トップページに3つのリンクが表示されます。

Go to about page (will redirect)

1つの目リンクを押します。
リンクは
http://localhost:3000/about
でしたが、

表示されたページは

とリダイレクトされました。

ファイルはこのファイルが表示されています。
pages/redirected.tsx

Go to another page (will rewrite)

2つ目のリンクを押します。

↑リンクはanotherで移動した先もanotherでした、

しかし表示されたページは

pages/rewrite.tsx
のページが表示されました。

解説

このコードは、TypeScriptで書かれたmiddlewareファイルの一部で、 requestオブジェクトのnextUrlプロパティのpathnameプロパティが"/another"と等しいかどうかをチェックします。

もし等しい場合、 requestオブジェクトと同じベースURLを持つパスが/rewriteの新しいURLオブジェクトを作成し、それをNextResponse.rewrite()メソッドに渡します。

このメソッドは、URLを新しいURLに書き換えるようにNext.jsに指示するNextResponseオブジェクトを返します。

Middlewareを使って基本的な関数の基礎を学ぶ

Middlewareに仕事をさせる前に関数の基礎について学びます。

middleware.ts
import { NextRequest, NextResponse } from "next/server";

export function middleware(request: NextRequest) {
  const url = request.url;
  console.log("middleware1 =>", { url });

  return NextResponse.next();
}

export const config = {
  matcher: ["/"],
};

http://localhost:3000/ をリロードすると
リクエストURLを Terminal に表示します。

現在Middlewareは request: NextRequest を受け取って NextResponse.next を投げているだけです。

次にpathnameをターミナルに表示してみます。

middleware.ts
import { NextRequest, NextResponse } from "next/server";

export function middleware(request: NextRequest) {
  const url = request.url;
  console.log("middleware1 =>", { url });

  const pathname = request.nextUrl.pathname;
  console.log("middleware2 =>", { pathname });

  return NextResponse.next();
}

export const config = {
  matcher: ["/"],
};

{ pathname } はオブジェクトの短縮記法であり、
キーと値が同じオブジェクトを作ります。

{ pathname } は{ pathname: pathname } と同じ意味です。

これでmiddlewareが2つ出来ました。

これを2つのmiddleware関数に分割してみます。

middleware.ts
import { NextRequest, NextResponse } from "next/server";

function middleware1(request: NextRequest) {
  const url = request.url;
  console.log("middleware1 =>", { url });

  return NextResponse.next();
}

function middleware2(request: NextRequest) {
  const pathname = request.nextUrl.pathname;
  console.log("middleware2 =>", { pathname });

  return NextResponse.next();
}

export function middleware(request: NextRequest) {
  middleware1(request);
  middleware2(request);
}

export const config = {
  matcher: ["/"],
};

NextResponse.next();
の真価がようやく発揮されました。

middleware1の処理が終わっても、次のmiddleware2を処理するための型が渡されます。
next()関数は次のmiddlewareを呼び出せるようにできる関数でした。
それがmiddleware2の引数 request になります。

ターミナルの出力を見ると結果は同じに表示されているはずです。

次にこれらを非同期に実行してみます。

middleware.ts
import { NextRequest, NextResponse } from "next/server";

async function middleware1(request: NextRequest) {
  const url = request.url;
  console.log("middleware1 =>", { url });

  return NextResponse.next();
}

async function middleware2(request: NextRequest) {
  const pathname = request.nextUrl.pathname;
  console.log("middleware2 =>", { pathname });

  return NextResponse.next();
}

export async function middleware(request: NextRequest) {
  await middleware1(request);
  await middleware2(request);
}

export const config = {
  matcher: ["/"],
};

非同期にしてもターミナルの出力は同じ結果になると思います。

middleware1 => { url: 'http://localhost:3000/' }
middleware2 => { pathname: '/' }

以上が一番シンプルな分割パターンです。

高階関数パターン

次に高階関数パターンを見てみます。
名前は手強そうですが、コードを追っていけば関数を重ねているだけだとわかります。

※console.logの出力値は。適当に選んでいます。

middleware.ts
import {
  NextFetchEvent,
  NextMiddleware,
  NextRequest,
  NextResponse,
} from "next/server";

// 以前のmiddleware1関数
// async function middleware1(request: NextRequest) {
//   const url = request.url;
//   console.log("middleware1 =>", { url });

//   return NextResponse.next();
// }

// ↑シンプルな分割から、↓高階関数用に書き換えます。
// 引数にmiddleware関数を受け取り、middleware関数を返す高階関数です。
function withMiddleware1(middleware: NextMiddleware) {
  return async (request: NextRequest, event: NextFetchEvent) => {
    const url = request.url;
    console.log("middleware1 =>", { url });

    return middleware(request, event);
  };
}

async function middleware2(request: NextRequest) {
  const pathname = request.nextUrl.pathname;
  console.log("middleware2 =>", { pathname });

  return NextResponse.next();
}

export default withMiddleware1(middleware2);

// export async function middleware(request: NextRequest) {
//   await middleware1(request);
//   await middleware2(request);
// }

// わかりやすくするため rootだけに設定しています。
export const config = {
  matcher: ["/"],
};

middleware1()を一旦消して、withMiddleware1()を高階関数として定義しました。

高階関数は関数を渡して関数を返してもらうので、コードを見るとreturn文が2つあるので奇妙に見えますが、関数そのものを返しているので単なる返り値です。

↓返り値はwithMiddleware1が受け取ったmiddleware2関数を実行して、middleware2の実行結果を返しています。

return middleware(request, event);

return では responseではなく、requestを返しています。

※Reactのバケツリレーのようなイメージでしょうか。
すべてのwithMiddleware*にリクエストを渡しています。

これも出力を見ると、前回の出力と同じに表示されているはずです。

MiddlewareがNext.jsやブラウザに返すのは NextResponse型です。

MiddlewareからMiddlewareに渡すのは NextMiddleware型です。

3番目のMiddlewareを作る場合は先ほどと同じです。

middleware.ts
// middleware3をまだ作ってないので動きません。
import {
  NextFetchEvent,
  NextMiddleware,
  NextRequest,
  NextResponse,
} from "next/server";

// async function middleware1(request: NextRequest) {
//   const url = request.url;
//   console.log("middleware1 =>", { url });

//   return NextResponse.next();
// }

function withMiddleware1(middleware: NextMiddleware) {
  return async (request: NextRequest, event: NextFetchEvent) => {
    const url = request.url;
    console.log("middleware1 =>", { url });

    return middleware(request, event);
  };
}

async function middleware2(request: NextRequest) {
  const pathname = request.nextUrl.pathname;
  console.log("middleware2 =>", { pathname });

  return NextResponse.next();
}

function withMiddleware2(middleware: NextMiddleware) {
  return async (request: NextRequest, event: NextFetchEvent) => {
    const pathname = request.nextUrl.pathname;
    console.log("middleware2 =>", { pathname });

    return middleware(request, event);
  };
}

// export default withMiddleware1(middleware2);
export default withMiddleware1(withMiddleware2(middleware3));
// 高階関数でラップすることでmiddlewareをチェーンできます。
// しかしまだ、↑middleware3をまだ作ってないので動きません。

// export async function middleware(request: NextRequest) {
//   await middleware1(request);
//   await middleware2(request);
// }

export const config = {
  matcher: ["/"],
};

複数個のMiddlewareをchain関数でつなげる。

引数にMiddlewareを入れ子にしていくのは無理があるので
別フォルダを作ってそこから呼び出せるようにします。

root直下もしくはsrcの下に middlewares フォルダを作成します。

複数のMiddlewareを受け取るベースを作ります。

src/middlewares/chain.ts
import { NextMiddleware, NextResponse } from "next/server";

type MiddlewareFactory = (middleware: NextMiddleware) => NextMiddleware;

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

  if (current) {
    const next = chain(functions, index + 1);
    return current(next);
  }

  return () => NextResponse.next();
}

これでMiddlewareを繰り返し読み込み、そして返す関数が出来たので

次は
独立したMiddlewareの機能(withMiddleware1,withMiddleware2)をmiddlewaresフォルダの下に移動します。

src/middlewares/middleware1.ts
import { NextMiddleware, NextRequest, NextFetchEvent } from "next/server";

export function withMiddleware1(middleware: NextMiddleware) {
  return async (request: NextRequest, event: NextFetchEvent) => {
    const url = request.url;
    console.log("middleware1 =>", { url });

    return middleware(request, event);
  };
}

src/middlewares/middleware2.ts
import { NextMiddleware, NextRequest, NextFetchEvent } from "next/server";

export function withMiddleware2(middleware: NextMiddleware) {
  return async (request: NextRequest, event: NextFetchEvent) => {
    const pathname = request.nextUrl.pathname;
    console.log("middleware2 =>", { pathname });

    return middleware(request, event);
  };
}

次はこれらを ↓ src/middleware.ts で読み込めるようにします。

src/middleware.ts
import { chain } from "./middlewares/chain";
import { withMiddleware1 } from "./middlewares/middleware1";
import { withMiddleware2 } from "./middlewares/middleware2";

export default chain([withMiddleware1, withMiddleware2]);

export const config = {
  matcher: ["/"],
};

これで http://localhost:3000/ を開くと今までと同じ結果が表示されると思います。

middleware1 => { url: 'http://localhost:3000/' }
middleware2 => { pathname: '/' }

この方法なら
export default chain([withMiddleware1, withMiddleware2]);
ここらへんの配列をいじればMiddlewareを1つづつ動作確認とか出来ますね。
まぁ本来ならテストを書けって話なんですが・・・

これで複数のMiddlewareを設定する方法は終了です。

新しいMiddlewareの機能を追加したければ、
middlewaresフォルダにmiddleware[?].tsファイルを追加すればいいだけです。

便利なMiddleware機能

参考サイトからコードを引っ張ってきます。

IP制限

widthIpRestriction.ts
import {
  NextFetchEvent,
  NextMiddleware,
  NextRequest,
  NextResponse,
} from "next/server";

export function widthIpRestriction(middleware: NextMiddleware) {
  return async (request: NextRequest, event: NextFetchEvent) => {
    // ##################################################
    // IP制限
    //
    // 全てのパスに対してIP制限を実行します。
    // ##################################################
    // ホワイトリストに登録されたIPリストを取得します。
    const ipWhiteList = new Set(
      process.env.IP_WHITE_LIST?.split(",").map((item: string) => {
        return item.trim();
      })
    );
    // ホワイトリストに登録されていないIPアドレスからのアクセスは拒否します。
    if (request.ip && !ipWhiteList.has(request.ip as string)) {
      const log = {
        message: `許可されていないIPアドレスからのアクセスのためアクセスを拒否しました`,
        ip: request.ip,
        url: request.nextUrl.pathname,
        method: request.method,
      };
      console.log(log);
      return new NextResponse(null, { status: 401 });
    }

    return middleware(request, event);
  };
}

ホワイトリスト

.env
IP_WHITE_LIST=xxx.xxx.xxx.xxx, yyy.yyy.yyy.yyy

解説

このコードは、IP 制限を実装するための関数です。IP 制限を行うことで、許可された IP アドレス以外からのアクセスを拒否することができます。

この関数では、環境変数 IP_WHITE_LIST に登録された IP アドレスのリストを取得し、リストに含まれていない IP アドレスからのアクセスを拒否します。また、アクセスが拒否された場合には、ログを出力して 401 Unauthorized ステータスコードを返します。

この関数を使用することで、簡単に IP 制限を実装することができます。また、環境変数を使用して IP アドレスのリストを管理するため、柔軟に設定を変更することができます。

ただし、IP 制限を行うため、許可された IP アドレスからのアクセスでも、制限に引っかかる可能性があります。また、環境変数を使用しているため、設定が誤っている場合には、誤った IP アドレスからのアクセスを許可してしまう可能性があります。

ブラックリスト方式にするには、条件式ifを反転させるだけです。(未実証)

ログ出力

withLogging.ts
import { NextFetchEvent, NextMiddleware, NextRequest } from "next/server";

export function widthLogging(middleware: NextMiddleware) {
  return async (request: NextRequest, event: NextFetchEvent) => {
    // ##################################################
    // ログ出力
    //
    // パスが以下の場合にログを出力します。
    // ・"/"から始まり、"."を含まない任意のパス
    // ・"/_nextから始まらない任意のパス
    // ・"/"のルートパス。
    // ・"/api"から始まる任意のパス
    // ・"/trpc"から始まる任意のパス
    // ##################################################
    if (
      request.nextUrl.pathname.match(/\/(?!.*\..*|_next).*/) ||
      request.nextUrl.pathname.match(/\/(api|trpc)(.*)/) ||
      request.nextUrl.pathname === "/"
    ) {
      // リクエストの情報をJSON形式で出力します。
      const log = {
        ip: request.ip,
        geo: request.geo,
        url: request.nextUrl.pathname,
        method: request.method,
      };
      console.log(JSON.stringify(log, (k, v) => (v === undefined ? null : v)));
    }

    return middleware(request, event);
  };
}

解説

このコードは、ログ出力を行うための関数です。この関数では、特定のパスにアクセスされた場合にログを出力します。

ログを出力する条件は以下の通りです。

  • パスが "/" から始まり、"." を含まない任意のパス
  • パスが "/_next" から始まらない任意のパス
  • パスが "/" のルートパス
  • パスが "/api" から始まる任意のパス
  • パスが "/trpc" から始まる任意のパス

ログ出力の際には、リクエストの情報を JSON 形式で出力します。出力する情報は、IP アドレス、地理情報、URL、HTTP メソッドです。

i18n

middleware.ts
import { NextResponse } from "next/server"
import acceptLanguage from "Accept-Language"

import { fallbackLng, languages, cookieName } from "./app/i18n/settings"

acceptLanguage.languages(languages)

export const config = {
  // matcher: '/:lng*'
  matcher: ["/((?!api|_next/static|_next/image|assets|favicon.ico|sw.js).*)"],
}

export function middleware(req) {
  let lng
  if (req.cookies.has(cookieName)) lng = acceptLanguage.get(req.cookies.get(cookieName).value)
  if (!lng) lng = acceptLanguage.get(req.headers.get("Accept-Language"))
  if (!lng) lng = fallbackLng

  // Redirect if lng in path is not supported
  if (
    !languages.some((loc) => req.nextUrl.pathname.startsWith(`/${loc}`)) &&
    !req.nextUrl.pathname.startsWith("/_next")
  ) {
    return NextResponse.redirect(new URL(`/${lng}${req.nextUrl.pathname}`, req.url))
  }

  if (req.headers.has("referer")) {
    const refererUrl = new URL(req.headers.get("referer"))
    const lngInReferer = languages.find((l) => refererUrl.pathname.startsWith(`/${l}`))
    const response = NextResponse.next()
    if (lngInReferer) response.cookies.set(cookieName, lngInReferer)
    return response
  }

  return NextResponse.next()
}

解説

このコードは、i18n(国際化)のためのミドルウェアです。このミドルウェアでは、クッキーや Accept-Language ヘッダーから言語を取得し、言語に応じたリダイレクトを行います。

まず、 Accept-Language パッケージを使用して、言語の設定を行います。次に、 config オブジェクトを定義して、matcher を設定します。matcher は、どの URL にこのミドルウェアを適用するかを指定するものです。

次に、 middleware 関数を定義しています。この関数では、クッキーや Accept-Language ヘッダーから言語を取得し、言語に応じたリダイレクトを行います。

まず、クッキーから言語を取得します。クッキーが存在しない場合は、 Accept-Language ヘッダーから言語を取得します。どちらからも取得できない場合に備えて、 fallbackLng にデフォルトの言語を設定します。

次に、リクエストされた URL がサポートされている言語でない場合は、言語に応じた URL にリダイレクトします。

最後に、referer ヘッダーから言語を取得し、クッキーに設定します。




ここより下は、Nexe.js公式ドキュメントの内容と解説です。

Middleware (App Router用)

Routing: Middleware | Next.js

※現在Next.jsにはApp RouterとPage RouterそれぞれMiddlewareの
ドキュメントがあります。

Middlewareとは?

Middleware は、
ユーザーからのリクエストに対して、レスポンスを返す前に、Middlewareで様々な処理を行うことができます。

例えば

  • リダイレクト
  • リクエスト
  • レスポンスを書き換えが出来ます。
  • レスポンスのヘッダーを変更出来ます。
  • 直接レスポンスを返す事が出来ます。
  • 認証済みかどうかを確認して、リクエストを処理するかどうかを決定することができます。
  • リクエストに基づいて、レスポンスに対する処理を行うことができます。

このように、middlewareを使用することで、Webアプリケーションの動作をより細かく制御することができます。

Middlewareを定義する場所

Middleware(Middleware)を定義するには、プロジェクトのルートにある middleware.ts(または .js)というファイルを使います。例えば、 pagesapp と同じレベルに置くか、該当する場合は src フォルダの中に置きます。

Middlewareは、Webアプリケーションの動作をより細かく制御することができます。例えば、リクエストやレスポンスを変更したり、リダイレクトしたりすることができます。

以下は、 middleware.ts ファイルの例です。

import { NextResponse } from 'next/server'
import type { NextRequest } from 'next/server'

export function myMiddleware(req: NextRequest) {
  // リクエストに対する処理を行う
  // ...

  // レスポンスを返す
  return NextResponse.next()
}

このように、 middleware.ts ファイルにMiddlewareを定義することで、Webアプリケーションの動作をカスタマイズすることができます。

Example

middleware.ts
import { NextResponse } from 'next/server'
import type { NextRequest } from 'next/server'

// middleware関数は、 `await`を使用している場合は`async`を付けることができます。
export function middleware(request: NextRequest) {
  return NextResponse.redirect(new URL('/home', request.url))
}

// マッチするリクエストを設定します
export const config = {
  matcher: '/about/:path*',
}

このコードは、 /about/:path*にマッチするリクエストがあった場合に、 /homeにリダイレクトするMiddleware関数を定義しています。

具体的には、 middleware関数内で、 NextResponse.redirectメソッドを使用して、リダイレクト先のURLを指定しています。

NextResponse.redirect(new URL('/home', request.url))

request.urlは、クライアントから送信されたリクエストのURLを表します。つまり、サーバーに送信されたリクエストのURLです。


aboutページにアクセスした場合
http://localhost:3000/about?lang=ja

homeページにリダイレクトします。
http://localhost:3000/home?lang=ja

?lang=ja <<この部分はクエリパラメータで
このクエリパラメータ部分は 書き換え後 も維持されています。

このMiddleware関数は、 exportされているため、他のファイルからインポートして使用することができます。また、 configオブジェクトを定義することで、このMiddleware関数をどのようなリクエストに対して適用するかを設定することができます。

configオブジェクト

middwareではconfigオブジェクトを設定しておけば、自動的に読み込んでくれます。

例えば、

    /*
     * Match all request paths except for the ones starting with:
     * - api (API routes)
     * - _next/static (static files)
     * - _next/image
     * - assets
     * - favicon.ico (favicon file)
     * - sw.js (Service Worker file)
     */
  matcher: ["/((?!api|_next/static|_next/image|assets|favicon.ico|sw.js).*)"]

このようにマッチャーを設定しておけば、自動的に読み込んでくれて、このパスにマッチしない(?!(否定先読み)記号を利用しているため)パスに対して処理を行います。

Matching Paths

マッチングパス

Middlewareは、プロジェクト内のすべてのルートに対して起動されます。
Next.jsでは1-8までがすべてのルートに相当します。

Middlewareが動く順番は、まず最初に1から8の順にルートを見ていきます。
そのルートがMiddlewareのmatcherに一致した場合、 Middleware内のコードが実行されます。

実行順序は以下の通りです:
1-8までがNext.jsのすべてのルートに相当します。

  1. next.config.jsからのヘッダー
  2. next.config.jsからのリダイレクト
  3. Middleware(リライト、リダイレクトなど)
  4. next.config.jsからのbeforeFiles(リライト)
  5. ファイルシステムのルート(public/、_next/static/、pages/、app/など)
  6. afterFiles (next.config.jsから書き換え)
  7. 動的ルート (/blog/[slug])
  8. fallback(next.config.jsからの書き換え)

8 fallback は、 matcher に対応するパスが存在しなかった場合に行う処理です。
つまり、 1-7 にマッチしないパスはすべて 8. fallback が処理を行います。
8 fallback は、アルゴリズム番兵法の番兵のようなものと言えます。

まず、リダイレクト処理はmiddlewareの役割というよりも、next.config.js に書いていました。
(middleware は Next.js v12からβ版が追加されました。)

だからまずnext.config.jsでリダイレクトを処理して、次にMiddlewareが処理するという流れになっています。

なので、↑2 のnext.config.jsを使ったパスがマッチして、リダイレクトが行われた場合、
↑3 のmiddleware より下位にあるパスは無視されます。

参考 next.config.jsを使ったリダイレクト

【Next.js】App Routerでのリダイレクトを模索した

next.config.jsで出来るリダイレクト
パス
Header
Cookie

middleware v13.0 から redirect() という関数が使えます。
この redirect() はServer Components、 Client Componentsの両方で使えます。

import { redirect } from 'next/navigation'

パスの定義

Middlewareが実行されるパスを定義するには、2つの方法があります:

  1. カスタムマッチャー構成
  2. 条件文

1. カスタムマッチャー構成

Matcher

matcher を使用すると、特定のパスで実行するミドルウェアをフィルタリングできます。

一番シンプルな例

middleware.ts
export const config = {
  matcher: '/about/:path*',
}

配列で、単一のパスまたは複数のパスにマッチさせることができます。

複数のマッチングが可能

middleware.ts
export const config = {
  matcher: ['/about/:path*', '/dashboard/:path*'],
}

matcher configでは正規表現が使用できます。

middleware.ts
    /*
     * Match all request paths except for the ones starting with:
     * - api (API routes)
     * - _next/static (static files)
     * - _next/image (image optimization files)
     * - assets (assets files)
     * - favicon.ico (favicon file)
     * - sw.js (Service Worker file)
     */
export const config = {
  matcher: ["/((?!api|_next/static|_next/image|assets|favicon.ico|sw.js).*)"]
}

例えば、 matcherプロパティには、リクエストに対してマッチするパスを指定することができます。

この例では、 /((?!api|_next/static|_next/image|assets|favicon.ico|sw.js).*)という正規表現を使用して、

api (API routes)
_next/static (static files)
_next/image (image optimization files)
assets (assets files)
favicon.ico (favicon file)
sw.js (Service Worker file)
これ以外のすべてのリクエストに対してマッチします。

Tips

マッチャーの値は、ビルド時に静的に解析できるように定数が好ましいです。動的な値は無視されます。

Configured matchers:

URLパスのマッチングに関するルール

  • パスは/で始まる必要があります。

  • 名前付きパラメータを含むことができます。たとえば、 /about/:path/about/a/about/bにマッチしますが、 /about/a/cにはマッチしません。

  • 名前付きパラメータには、修飾子を付けることができます。たとえば、 /about/:path*は、 /about/a/b/cにもマッチします。*は0回以上の繰り返しを表します。?は0回または1回の繰り返しを表します。+は1回以上の繰り返しを表します。

  • 正規表現を使用することができます。たとえば、 /about/(.*)は、 /about/:path*と同じように動作します。

これらのルールを使用することで、URLパスに対する柔軟なマッチングを実現することができます。

名前付きパラメータを使用する場合には、パラメータ名が一意であることが必要です。

pillarjs/path-to-regexp: Turn a path string such as /user/:name into a regular expression

↑正規表現のルール

Tips

後方互換性のため、Next.jsは常に/publicを/public/indexとみなします。
したがって、/public/:pathのマッチャーはマッチします。

2. 条件文

Conditional Statements

middleware.ts
import { NextResponse } from 'next/server'
import type { NextRequest } from 'next/server'

export function middleware(request: NextRequest) {
  if (request.nextUrl.pathname.startsWith('/about')) {
    return NextResponse.rewrite(new URL('/about-2', request.url))
  }

  if (request.nextUrl.pathname.startsWith('/dashboard')) {
    return NextResponse.rewrite(new URL('/dashboard/user', request.url))
  }
}

リクエストに対して条件文で2種類のURLの書き換えを行っています。

具体的には、middleware関数は、requestオブジェクトを受け取り、if文でリクエストされたURLのパスに応じて、URLの書き換えを行っています。

/aboutで始まるパスの場合は、/about-2に書き換え、
/dashboardで始まるパスの場合は、/dashboard/userに書き換えています。

URLの書き換えには、NextResponse.rewriteメソッドを使用しています。

このメソッドは、引数に書き換え後のURLを指定することで、リクエストされたURLを書き換えることができます。

NextResponse

Next.jsのNextResponse APIについて以下のような機能があります。

  • リダイレクト:リクエストを別のURLにリダイレクトすることができます。

  • レスポンスの書き換え:指定されたURLを表示することで、レスポンスを書き換えることができます。

  • リクエストヘッダーの設定:APIルート、getServerSideProps、および書き換え先に対してリクエストヘッダーを設定することができます。

  • レスポンスクッキーの設定:レスポンスにクッキーを設定することができます。

  • レスポンスヘッダーの設定:レスポンスにヘッダーを設定することができます。

Middlewareからレスポンスを生成する方法としては、以下の2つがあります。

  • ルート(ページまたはルートハンドラー)に書き換えて、レスポンスを生成する。

  • NextResponseオブジェクトを直接返します。
    詳細については、ここより↓にあるProducing a Responseを参照してください。

Routing: Pages and Layouts | Next.js

Routing: Route Handlers | Next.js

Using Cookies

クッキーは、リクエストのCookieヘッダーまたはレスポンスのSet-Cookieヘッダーに格納されます。

リクエストのCookieヘッダー
クライアントからサーバーに送信されるHTTPリクエストに含まれるヘッダーの一つで、クライアントがサーバーに送信するクッキー情報を含んでいます。

レスポンスのSet-Cookieヘッダー
サーバーからクライアントに送信されるHTTPレスポンスに含まれるヘッダーの一つで、サーバーがクライアントに送信するクッキー情報を含んでいます。

Set-Cookieヘッダーには、クッキーの有効期限、ドメイン、パス、セキュア属性などの情報を含めることができます。

クッキー情報は、名前と値のペアで構成され、Webサイトの訪問履歴やログイン情報などを保存するために使用されます。

Next.jsでは、 NextRequestNextResponseの拡張機能を使用して、クッキーを簡単にアクセスおよび操作することができます。

middleware.ts
import { NextResponse } from "next/server"
import type { NextRequest } from "next/server"

export function middleware(request: NextRequest) {
  // "Cookie:nextjs=fast"ヘッダーが受信リクエストに存在すると仮定する
  // RequestCookies APIを使用してリクエストからクッキーを取得する
  let cookie = request.cookies.get("nextjs")
  console.log(cookie) // => { name: 'nextjs', value: 'fast', Path: '/' }
  const allCookies = request.cookies.getAll()
  console.log(allCookies) // => [{ name: 'nextjs', value: 'fast' }]

  request.cookies.has("nextjs") // => true
  request.cookies.delete("nextjs")
  request.cookies.has("nextjs") // => false

  // `ResponseCookies` APIを使用してレスポンスにクッキーを設定する
  const response = NextResponse.next()
  response.cookies.set("vercel", "fast")
  response.cookies.set({
    name: "vercel",
    value: "fast",
    path: "/",
  })
  cookie = response.cookies.get("vercel")
  console.log(cookie) // => { name: 'vercel', value: 'fast', Path: '/' }
  // ja: 出力レスポンスには `Set-Cookie:vercel=fast;path=/test` ヘッダーが含まれます。

  return response
}

↑このコードには、
リクエストからクッキーを取得する方法
レスポンスにクッキーを設定する方法
が書かれています。

  • リクエストからクッキーを操作する方法は
    request.cookies.getメソッドを使用して、クッキーの値を取得することができます。
    request.cookies.getAllメソッドを使用して、すべてのクッキーを取得することができます。
    request.cookies.hasメソッドを使用して、クッキーが存在するかどうかを確認します。
    request.cookies.deleteメソッドを使用して、クッキーを削除します。

  • レスポンスにクッキーを設定する場合は
    response.cookies.setメソッドを使用して、クッキーを設定します。
    また、オブジェクトを渡すことで、複数のクッキーを一度に設定することもできます。
    response.cookies.getメソッドを使用して、設定されたクッキーの値を取得します。

Setting Headers

リクエストヘッダとレスポンスヘッダは、NextResponse APIで設定できます。
(リクエストヘッダの設定はNext.js v13.0.0から)

middleware.ts
import { NextResponse } from "next/server"
import type { NextRequest } from "next/server"

export function middleware(request: NextRequest) {
  // リクエストヘッダーをクローンし、新しいヘッダー `x-hello-from-middleware1` を設定します。
  const requestHeaders = new Headers(request.headers)
  requestHeaders.set("x-hello-from-middleware1", "hello")

  // NextResponse.rewrite でもリクエストヘッダーを設定できます。
  const response = NextResponse.next({
    request: {
      // 新しいリクエストヘッダー
      headers: requestHeaders,
    },
  })

  // 新しいレスポンスヘッダー `x-hello-from-middleware2` を設定します。
  response.headers.set("x-hello-from-middleware2", "hello")
  return response
}

↑このコードは、Next.jsでリクエストとレスポンスのヘッダーを設定する方法です。

  1. Headersオブジェクトを使用して、リクエストヘッダーをクローンし、新しいヘッダーを設定します。request.headersからヘッダーを取得し、 requestHeaders.setメソッドを使用して、新しいヘッダーを設定します。

  2. NextResponse.nextメソッドを使用して、新しいレスポンスを作成します。このとき、 requestオプションを使用して、新しいリクエストヘッダーを設定することができます。requestHeadersを使用して、新しいヘッダーを設定します。

  3. response.headers.setメソッドを使用して、新しいレスポンスヘッダーを設定します。
    クローン
    オブジェクトをクローンするとは、元のオブジェクトと同じプロパティを持つ新しいオブジェクトを作成することです。
    元のオブジェクトと新しいオブジェクトは、別々のメモリ領域に保存されます。
    クローンすることで、元のオブジェクトを変更しても、新しいオブジェクトには影響がなくなります。

このコードでは、 Headersオブジェクトをクローンして、新しいヘッダーを設定しています。Headersオブジェクトは、HTTPリクエストやレスポンスのヘッダーを表すオブジェクトです。
Headersオブジェクトをクローンすることで、元のリクエストヘッダーを変更することなく、新しいヘッダーを設定することができます。

注意点としては、ヘッダーのサイズが大きすぎる場合、バックエンドのWebサーバーの設定によっては、431 Request Header Fields Too Largeエラーが発生する可能性があることです。また、ヘッダーにはセキュリティ上の問題があるため、機密情報を含めないようにする必要があります。

この方法の長所は、リクエストとレスポンスのヘッダーを柔軟に設定できることです。

例えば、認証トークンをリクエストヘッダーに設定することで、APIの認証を行うことができます。
また、レスポンスヘッダーには、キャッシュ制御やセキュリティ関連の情報を設定することができます。

この方法は、APIの認証やキャッシュ制御、セキュリティ関連の情報を設定する場合に使用することができます。
また、リクエストとレスポンスのヘッダーを柔軟に設定することができるため、様々な用途に応用することができます。

※バックエンドのウェブサーバーの設定によっては、431 Request Header Fields Too Large エラーが発生する可能性があります。

Producing a Response

ResponseまたはNextResponseインスタンスを返すことで、Middlewareから直接応答できます。(これはNext.js v13.1.0から利用可能です。)

middleware.ts
import { NextRequest, NextResponse } from 'next/server'
import { isAuthenticated } from '@lib/auth'

// Limit the middleware to paths starting with `/api/`
export const config = {
  matcher: '/api/:function*',

}

export function middleware(request: NextRequest) {

  // Call our authentication function to check the request

  if (!isAuthenticated(request)) {

    // Respond with JSON indicating an error message

    return new NextResponse(
      JSON.stringify({ success: false, message: 'authentication failed' }),
      { status: 401, headers: { 'content-type': 'application/json' } }
    )
  }
}

↑このコードは、Next.jsでMiddlewareから直接レスポンスを返す方法です。

  1. configオブジェクトを使用して、Middlewareを/api/で始まるパスに制限します。
    これにより、このMiddlewareは/api/で始まるパスにのみ適用されます。

  2. isAuthenticated関数を使用して、リクエストが認証されているかどうかを確認します。
    認証に失敗した場合は、エラーメッセージを含むJSONレスポンスを返します。

  3. NextResponseクラスを使用して、新しいレスポンスを作成します。
    NextResponseクラスは、 bodyoptionsの2つの引数を受け取ります。

bodyはレスポンスの本文を表します。
optionsはレスポンスのオプションを表します。

このコードでは、 bodyにJSON文字列を、 optionsにステータスコードとヘッダーを設定しています。

この方法の長所は、Middlewareから直接レスポンスを返すことができるため、APIの認証やエラーハンドリングなど、様々な用途に応用することができます。

また、 NextResponseクラスを使用することで、レスポンスの本文やヘッダーを柔軟に設定することができます。

一方、短所としては、レスポンスを直接返すため、後続のMiddlewareやハンドラーが実行されないことがあることが挙げられます。また、レスポンスの本文やヘッダーを直接設定するため、コードが複雑になる可能性があることもあります。

この方法は、APIの認証やエラーハンドリングなど、Middlewareから直接レスポンスを返す必要がある場合に使用することができます。また、 NextResponseクラスを使用することで、レスポンスの本文やヘッダーを柔軟に設定することができます。

Advanced Middleware Flags

Next.jsのv13.1では、高度なユースケースに対応するために、

skipTrailingSlashRedirect
skipMiddlewareUrlNormalize

という2つの追加フラグがMiddlewareに導入されました。

これにより、直接アクセスとクライアント側の遷移を同じように扱うことができない場合に、元のURLを使用することができます。

skipTrailingSlashRedirect

Next.jsのデフォルトのリダイレクトを無効にして、末尾のスラッシュを追加または削除します。

※トレーリングスラッシュ
URLの末尾に付くスラッシュのことです。
例えば、 https://example.com/path/のように、末尾にスラッシュが付いている場合があります。
トレーリングスラッシュが付いているかどうかによって、同じページでも異なるURLとして扱われることがあります。

next.config.js
module.exports = {
  skipTrailingSlashRedirect: true,
}

middleware.ts
const legacyPrefixes = ['/docs', '/blog']

export default async function middleware(req) {
  const { pathname } = req.nextUrl

  if (legacyPrefixes.some((prefix) => pathname.startsWith(prefix))) {
    return NextResponse.next()
  }

  // apply trailing slash handling
  if (
    !pathname.endsWith('/') &&
    !pathname.match(/((?!\.well-known(?:\/.*)?)(?:[^/]+\/)*[^/]+\.\w+)/)
  ) {
    req.nextUrl.pathname += '/'
    return NextResponse.redirect(req.nextUrl)
  }
}

↑このコードは、Next.jsのMiddlewareを使用して、トレーリングスラッシュの処理をカスタマイズする方法を示しています。

  1. skipTrailingSlashRedirectフラグを使用して、Next.jsのデフォルトのリダイレクトを無効にします。
    これにより、Middleware内でカスタムな処理を行うことができます。例えば、一部のパスにはトレーリングスラッシュを付け、他のパスには付けないようにすることができます。これにより、徐々に移行することができます。

  2. middleware関数を定義し、 reqオブジェクトからpathnameを取得します。

  3. legacyPrefixes配列に含まれるパスの場合は、 NextResponse.next()を返して、後続のMiddlewareやハンドラーが実行されるようにします。

  4. トレーリングスラッシュの処理を行います。pathnameがスラッシュで終わっていない場合、かつ.well-knownで始まるパスでない場合は、 req.nextUrl.pathnameにスラッシュを追加して、 NextResponse.redirect()を返します。これにより、トレーリングスラッシュが付いたURLにリダイレクトされます。

この方法の長所は、Middlewareでより柔軟なトレーリングスラッシュの処理を行うことができることです。例えば、一部のパスにはトレーリングスラッシュを付け、他のパスには付けないようにすることができます。

また、 skipTrailingSlashRedirectフラグを使用することで、Next.jsのデフォルトの挙動から外れることができます。

一方、短所としては、トレーリングスラッシュの処理をカスタマイズするため、コードが複雑になる可能性があることが挙げられます。

この方法は、トレーリングスラッシュの処理をカスタマイズする必要がある場合に使用することができます。例えば、一部のパスにはトレーリングスラッシュを付け、他のパスには付けないようにすることができます。また、 skipTrailingSlashRedirectフラグを使用することで、Next.jsのデフォルトの挙動から外れることができます。ただし、トレーリングスラッシュの処理をカスタマイズする場合は、コードが複雑になる可能性があることに注意する必要があります。

skipMiddlewareUrlNormalize

Next.jsが行うURL正規化を無効にし、直接訪問とクライアント遷移の処理を同じにします。
元のURLを使用して完全に制御する必要がある高度なケースもありますが、そのような場合はこの機能を利用できます。

next.config.js
module.exports = {
  skipMiddlewareUrlNormalize: true,
}

middleware.ts
export default async function middleware(req) {
  const { pathname } = req.nextUrl

  // GET /_next/data/build-id/hello.json

  console.log(pathname)
  // with the flag this now /_next/data/build-id/hello.json
  // without the flag this would be normalized to /hello
}

↑このコードは、Next.jsのMiddlewareを使用して、URLの正規化処理をカスタマイズする方法です。

  1. skipMiddlewareUrlNormalizeフラグを使用して、Next.jsがURLを正規化する処理を無効にします。
    これにより、直接アクセスとクライアント側の遷移を同じように扱うことができない場合に、元のURLを使用することができます。

  2. middleware関数を定義し、 reqオブジェクトからpathnameを取得します。

  3. console.log()を使用して、 pathnameを出力します。
    skipMiddlewareUrlNormalizeフラグを使用している場合、 pathnameは正規化されずにそのまま出力されます。
    一方、フラグを使用していない場合は、 pathnameが正規化されてしまい、元のURLと異なる場合があります。

この方法の長所は、URLの正規化処理をカスタマイズすることができることです。
例えば、直接アクセスとクライアント側の遷移を同じように扱うことができない場合に、元のURLを使用することができます。

一方、短所としては、フラグを使用することで、Next.jsのデフォルトの挙動から外れるため、予期しない動作を引き起こす可能性があることが挙げられます。
また、フラグを使用することで、コードが複雑になる可能性があることもあります。

この方法は、URLの正規化処理をカスタマイズする必要がある場合に使用することができます。ただし、フラグを使用する場合は、予期しない動作に注意する必要があります。

Middleware version History

Version Changes
v13.1.0 Advanced Middleware flags added
v13.0.0 Middleware can modify request headers, response headers, and send responses
v12.2.0 Middleware is stable, please see the upgrade guide
v12.0.9 Enforce absolute URLs in Edge Runtime (PR)
v12.0.0 Middleware (Beta) added

v13.0.0
Middlewareはリクエスト ヘッダー、レスポンス ヘッダーを変更し、レスポンスを送信できます。

参考

Next.jsで複数のmiddlewareの関数を連結させる

hayato94087/nextjs-middleware-chain-sample

Chain Multiple Middleware Functions in NextJs 13 - YouTube

path 判定の Tips

↓現在位置が/aboutであるという判断に使用します。

req.nextUrl.pathname === "/about"

↓現在位置が/aboutかもしくはその下であるという判断に使用します。

req.nextUrl.pathname === "/about/*"

↓pathの始まりが/aboutで始まっているかどうかの判断に使用します。

request.nextUrl.pathname.startsWith('/about')

↓動的フォルダ以下であるかどうかの判断 (プレースホルダー)
matcher: '/:lng*'

"*" 記号はワイルドカード(≒正規表現)です、0回以上の繰り返しにマッチします。

参考

missing の実装例

Next.jsでMiddlewareが大量に実行される場合の対処法
https://zenn.dev/sasatech/articles/035e634d7ea8c3




マニュアル再調査、再翻訳の追加 2024年4月3日

Routing: Middleware | Next.js
https://nextjs.org/docs/app/building-your-application/routing/middleware

使用例

ミドルウェアをアプリケーションに統合することで、パフォーマンス、セキュリティ、ユーザーエクスペリエンスを大幅に向上させることができます。
ミドルウェアが特に効果を発揮する一般的なシナリオには、以下のようなものがあります。

認証と認可: 特定のページやAPIルートへのアクセスを許可する前に、ユーザーの身元を確認し、セッションクッキーをチェックします。

サーバーサイド・リダイレクト: 特定の条件(ロケール、ユーザーの役割など)に基づいて、サーバーレベルでユーザーをリダイレクトします。

パスの書き換え: リクエストプロパティに基づいてAPIルートやページへのパスを動的に書き換えることで、A/Bテスト、機能ロールアウト、レガシーパスをサポートします。

ボット検知: ボットトラフィックを検出してブロックすることで、リソースを保護します。

ロギングと分析: ページまたはAPIで処理する前に、リクエストデータを取得して分析し、洞察を得ます。

機能フラグ: シームレスな機能展開やテストのために、動的に機能を有効または無効にします。

注意すべき使用方法

ミドルウェアが最適なアプローチではないかもしれない状況を認識することも、同様に重要です。

複雑なデータの取得と操作: ミドルウェアは、直接データを取得したり操作したりするようには設計されていません。
このようなことは、Route Handlersやサーバーサイドのユーティリティで行うべきです。

重い計算タスク: ミドルウェアは軽量で応答が速いものでなければなりません。
重い計算タスクや長時間実行される処理は、専用のRoute Handler内で行うべきです。

広範なセッション管理: ミドルウェアは基本的なセッションタスクを管理できますが、広範なセッション管理は専用の認証サービスかRoute Handlers内で管理する必要があります。

データベースの直接操作: Middleware内でデータベースを直接操作することは推奨しません。データベースとのやり取りは、Route Handlersまたはサーバサイドのユーティリティ内で行う必要があります。

Middlewareの入出力

入力

request
型 NextRequest

requestを受け取ります。

出力

response

responseを返します。

Next.jsアプリか、ブラウザに返す。
型 NextResponse

NextResponseクラスメソッド

json

通常はオブジェクト形式で送受信しますが、Json形式に変更してやり取りをします。受け取る側は自動的にデコードをしてくれます。

redirect
rewrite
next

次のMiddlewareに渡す事が出来ます。

Note: While only one middleware.ts file is supported per project, you can still organize your middleware logic modularly. Break out middleware functionalities into separate .ts or .js files and import them into your main middleware.ts file. This allows for cleaner management of route-specific middleware, aggregated in the middleware.ts for centralized control. By enforcing a single middleware file, it simplifies configuration, prevents potential conflicts, and optimizes performance by avoiding multiple middleware layers.

ja: 注:1つのプロジェクトにつき1つのmiddleware.tsファイルしかサポートされていませんが、ミドルウェアのロジックをモジュール式に整理することは可能です。

ミドルウェアの機能を個別の.tsまたは.jsファイルに分割し、メインのmiddleware.tsファイルにインポートします。

これにより、ルート固有のミドルウェアをmiddleware.tsに集約して一元管理することができます。

単一のミドルウェアファイルを強制することで、設定を簡素化し、潜在的な競合を防ぎ、複数のミドルウェアレイヤーを避けることでパフォーマンスを最適化します。

middleware

次のMiddlewareにわたす。
NextMiddleware


matcher

Routing: Middleware | Next.js
https://nextjs.org/docs/app/building-your-application/routing/middleware#convention

特定のpathに絞り込みます。

matcherでpathを絞り込み、条件文で特定の機能を実行させます。

基本

middleware.ts
export const config = {
  matcher: '/about/:path*',
}

↓配列でも利用可能

middleware.ts
export const config = {
  matcher: ['/about/:path*', '/dashboard/:path*'],
}

  • はワイルドカードで、about 以下であればどのようなパスでもマッチ します。
    以下のパスはすべてマッチします。

/about/
/about/me
/about/team
/about/contact
/about/products/123

正規表現も利用可能です。

/about/(.*)
と
/about/:path*

は同じです。

has missing

middleware.ts
export const config = {
  matcher: [
    /*
     * Match all request paths except for the ones starting with:
     * - api (API routes)
     * - _next/static (static files)
     * - _next/image (image optimization files)
     * - favicon.ico (favicon file)
     */
    {
      source: '/((?!api|_next/static|_next/image|favicon.ico).*)',
      missing: [
        { type: 'header', key: 'next-router-prefetch' },
        { type: 'header', key: 'purpose', value: 'prefetch' },
      ],
    },

    {
      source: '/((?!api|_next/static|_next/image|favicon.ico).*)',
      has: [
        { type: 'header', key: 'next-router-prefetch' },
        { type: 'header', key: 'purpose', value: 'prefetch' },
      ],
    },

    {
      source: '/((?!api|_next/static|_next/image|favicon.ico).*)',
      has: [{ type: 'header', key: 'x-present' }],
      missing: [{ type: 'header', key: 'x-missing', value: 'prefetch' }],
    },
  ],
}

has、missing、もしくは両方を使って特定のリクエストができます。

missing プロパティ

これは、特定のヘッダーが存在しない リクエストに対してミドルウェアをスキップする方法です。

middleware.ts
missing: [
  { type: 'header', key: 'next-router-prefetch' },
  { type: 'header', key: 'purpose', value: 'prefetch' },
],

You can also ignore prefetches (from next/link) that don't need to go through the Middleware using the missing array:

また、(next/linkからの)ミドルウェアを経由する必要のないプリフェッチも、missing arrayを使って無視することができる:

middleware.ts
export const config = {
  matcher: [
    /*
     * Match all request paths except for the ones starting with:
     * - api (API routes)
     * - _next/static (static files)
     * - _next/image (image optimization files)
     * - favicon.ico (favicon file)
     */
    {
      source: '/((?!api|_next/static|_next/image|favicon.ico).*)',
      missing: [
        { type: 'header', key: 'next-router-prefetch' },
        { type: 'header', key: 'purpose', value: 'prefetch' },
      ],
    },
  ],
}

has プロパティ

こちらは、特定のヘッダーが存在する リクエストに対してミドルウェアをスキップする方法です。

middleware.ts
has: [
  { type: 'header', key: 'next-router-prefetch' },
  { type: 'header', key: 'purpose', value: 'prefetch' },
],

組み合わせ

middleware.ts
has: [{ type: 'header', key: 'x-present' }],
missing: [{ type: 'header', key: 'x-missing', value: 'prefetch' }],

静的な値

設定値は静的な値を使用します。

動的な値は無視されます。

publicフォルダ

Next.js では、後方互換性 のために、/public フォルダは常に /public/index.html として扱われます。

  • /public/index.html は、/public にアクセスした際に常に表示されます。
  • /public 以下の他のファイルは、直接アクセスすることで読み込むことができます。
  • /public/:path という matcher は、/public 以下の すべてのファイル にマッチします。
    matcherの設定でも同じです。

条件文

middleware.ts
import { NextResponse } from 'next/server'
import type { NextRequest } from 'next/server'

export function middleware(request: NextRequest) {
  if (request.nextUrl.pathname.startsWith('/about')) {
    return NextResponse.rewrite(new URL('/about-2', request.url))
  }

  if (request.nextUrl.pathname.startsWith('/dashboard')) {
    return NextResponse.rewrite(new URL('/dashboard/user', request.url))
  }
}

NextResponse

Next.js 13 のサーバーサイドミドルウェアで、レスポンスを生成・操作するための API が NextResponse です。この API を使うことで、以下のようなことが可能になります。

レスポンスの操作

redirect: リクエストを別の URL にリダイレクトさせる
rewrite: レスポンスを別の URL の内容で置き換える
Set request headers: API ルート、getServerSideProps、リライト先の URL に対してリクエストヘッダーを設定する
Set response cookies: レスポンスにクッキーを設定する
Set response headers: レスポンスヘッダーを設定する
レスポンスの生成方法

ミドルウェアでレスポンスを生成するには、以下の 2 つの方法があります。

rewrite: ページやルートハンドラーにリライトし、そこで生成されたレスポンスを返す
return NextResponse directly: NextResponse オブジェクトを直接返して、レスポンスを生成する (詳細は "Producing a Response" を参照)
まとめ

NextResponse API は、サーバーサイドミドルウェアにおけるレスポンスの操作と生成を強力にサポートする機能です。これにより、柔軟で高度なミドルウェアを開発することが可能になります。

Cookiesの使用

クッキーは通常のヘッダーです。リクエストでは、Cookieヘッダーに格納されます。レスポンスでは、Set-Cookieヘッダーに格納されます。Next.jsは、NextRequestとNextResponseのCookiesエクステンションを通して、これらのCookiesにアクセスし、操作する便利な方法を提供します。

着信リクエストに対して、Cookiesには次のメソッドが用意されています: get、getAll、set、delete Cookies。hasでクッキーの存在を確認したり、clearですべてのクッキーを削除したりできます。
発信レスポンスに対して、クッキーは以下のメソッドを持っています:get、getAll、set、delete。

middleware.ts
import { NextResponse } from 'next/server'
import type { NextRequest } from 'next/server'

export function middleware(request: NextRequest) {
  // Assume a "Cookie:nextjs=fast" header to be present on the incoming request
  // Getting cookies from the request using the `RequestCookies` API
  let cookie = request.cookies.get('nextjs')
  console.log(cookie) // => { name: 'nextjs', value: 'fast', Path: '/' }
  const allCookies = request.cookies.getAll()
  console.log(allCookies) // => [{ name: 'nextjs', value: 'fast' }]

  request.cookies.has('nextjs') // => true
  request.cookies.delete('nextjs')
  request.cookies.has('nextjs') // => false

  // Setting cookies on the response using the `ResponseCookies` API
  const response = NextResponse.next()
  response.cookies.set('vercel', 'fast')
  response.cookies.set({
    name: 'vercel',
    value: 'fast',
    path: '/',
  })
  cookie = response.cookies.get('vercel')
  console.log(cookie) // => { name: 'vercel', value: 'fast', Path: '/' }
  // The outgoing response will have a `Set-Cookie:vercel=fast;path=/` header.

  return response
}

Setting Headers

NextResponse API を使用してリクエスト ヘッダーとレスポンス ヘッダーを設定できます。

middleware.ts
import { NextResponse } from 'next/server'
import type { NextRequest } from 'next/server'

export function middleware(request: NextRequest) {
  // Clone the request headers and set a new header `x-hello-from-middleware1`
  const requestHeaders = new Headers(request.headers)
  requestHeaders.set('x-hello-from-middleware1', 'hello')

  // You can also set request headers in NextResponse.rewrite
  const response = NextResponse.next({
    request: {
      // New request headers
      headers: requestHeaders,
    },
  })

  // Set a new response header `x-hello-from-middleware2`
  response.headers.set('x-hello-from-middleware2', 'hello')
  return response
}

※バックエンド Web サーバーの構成によっては、「431 Request Header Fields Too Large」エラーが発生する可能性があるため、大きなヘッダーを設定しないでください。

CORS

ミドルウェアでCORSヘッダーを設定し、シンプルリクエストやプリフライトリクエストを含むクロスオリジンリクエストを許可することができます。

middleware.ts
import { NextRequest, NextResponse } from 'next/server'

const allowedOrigins = ['https://acme.com', 'https://my-app.org']

const corsOptions = {
  'Access-Control-Allow-Methods': 'GET, POST, PUT, DELETE, OPTIONS',
  'Access-Control-Allow-Headers': 'Content-Type, Authorization',
}

export function middleware(request: NextRequest) {
  // Check the origin from the request
  const origin = request.headers.get('origin') ?? ''
  const isAllowedOrigin = allowedOrigins.includes(origin)

  // Handle preflighted requests
  const isPreflight = request.method === 'OPTIONS'

  if (isPreflight) {
    const preflightHeaders = {
      ...(isAllowedOrigin && { 'Access-Control-Allow-Origin': origin }),
      ...corsOptions,
    }
    return NextResponse.json({}, { headers: preflightHeaders })
  }

  // Handle simple requests
  const response = NextResponse.next()

  if (isAllowedOrigin) {
    response.headers.set('Access-Control-Allow-Origin', origin)
  }

  Object.entries(corsOptions).forEach(([key, value]) => {
    response.headers.set(key, value)
  })

  return response
}

export const config = {
  matcher: '/api/:path*',
}

※ルートハンドラで個々のルートにCORSヘッダを設定できます。

Producing a Response

Responseの生成

middleware.ts
import { NextRequest } from 'next/server'
import { isAuthenticated } from '@lib/auth'

// Limit the middleware to paths starting with `/api/`
export const config = {
  matcher: '/api/:function*',
}

export function middleware(request: NextRequest) {
  // Call our authentication function to check the request
  if (!isAuthenticated(request)) {
    // Respond with JSON indicating an error message
    return Response.json(
      { success: false, message: 'authentication failed' },
      { status: 401 }
    )
  }
}

waitUntilとNextFetchEvent

NextFetchEventは、従来のFetchEventオブジェクトにwaitUntilメソッドを追加したものです。

waitUntilメソッドを使うと、Promiseをラップすることで、ミドルウェアのライフタイムをPromiseが解決されるまで延長することができます。これは、バックグラウンドで処理を実行する際に役立ちます。

waitUntil と NextFetchEventは、Next.jsのミドルウェアでバックグラウンド処理を実行する際に役立つ機能です。

middleware.ts
import { NextResponse } from 'next/server'
import type { NextFetchEvent, NextRequest } from 'next/server'

export function middleware(req: NextRequest, event: NextFetchEvent) {
  event.waitUntil(
    fetch('https://my-analytics-platform.com', {
      method: 'POST',
      body: JSON.stringify({ pathname: req.nextUrl.pathname }),
    })
  )

  return NextResponse.next()
}

Advanced Middleware Flags

Next.jsのv13.1では、高度なユースケースに対応するために、ミドルウェア用に2つの追加フラグ、skipMiddlewareUrlNormalizeとskipTrailingSlashRedirectが導入されました。

skipTrailingSlashRedirectは、Next.jsが末尾のスラッシュを追加または削除してリダイレクトするのを無効にします。これにより、あるパスでは末尾のスラッシュを維持し、他のパスでは維持しないような、ミドルウェア内部のカスタム処理が可能になります。

next.config.js
module.exports = {
  skipTrailingSlashRedirect: true,
}

middleware.ts
const legacyPrefixes = ['/docs', '/blog']

export default async function middleware(req) {
  const { pathname } = req.nextUrl

  if (legacyPrefixes.some((prefix) => pathname.startsWith(prefix))) {
    return NextResponse.next()
  }

  // apply trailing slash handling
  if (
    !pathname.endsWith('/') &&
    !pathname.match(/((?!\.well-known(?:\/.*)?)(?:[^/]+\/)*[^/]+\.\w+)/)
  ) {
    req.nextUrl.pathname += '/'
    return NextResponse.redirect(req.nextUrl)
  }
}

skipMiddlewareUrlNormalizeを使用すると、Next.jsのURL正規化を無効にして、直接訪問とクライアント遷移の処理を同じにすることができます。いくつかの高度なケースでは、このオプションは元のURLを使用することで完全な制御を提供します。

next.config.js
module.exports = {
  skipMiddlewareUrlNormalize: true,
}

middleware.ts
export default async function middleware(req) {
  const { pathname } = req.nextUrl

  // GET /_next/data/build-id/hello.json

  console.log(pathname)
  // with the flag this now /_next/data/build-id/hello.json
  // without the flag this would be normalized to /hello
}

Runtime

ミドルウェアは現在、Edgeランタイムのみをサポートしています。Node.jsランタイムは使用できません。




ファイル規則

ファイル規則: middleware.ts | Next.js
https://nextjs.org/docs/app/api-reference/file-conventions/middleware

サンプルのmatcher

middleware.ts
export const config = {
  matcher: [
    {
      source: '/api/*',
      regexp: '^/api/(.*)',
      locale: false,
      has: [
        { type: 'header', key: 'Authorization', value: 'Bearer Token' },
        { type: 'query', key: 'userId', value: '123' },
      ],
      missing: [{ type: 'cookie', key: 'session', value: 'active' }],
    },
  ],
}

source

普通の文字列を使ってパスを設定します。

export const config = {
matcher: ['/'],
};
これは

export const config = {
matcher: [
{
source: '/',
},
],
}

これと同じ意味です。

regexp

正規表現を使ってパスを設定します。

locale

falseに設定すると、パスマッチングにおいてロケールベースのルーティングを無視するブール値です。

has (オプション)

ヘッダ、クエリパラメータ、クッキーなどの特定のリクエスト要素の存在に基づく条件を指定します。

missing (オプション)

ヘッダやクッキーが存在しないなど、特定のリクエスト要素が存在しない場合の条件を指定します。

Middlewareにちょっと潜り込む

インポートしたNextMiddlewares を調べる

middleware.ts
import {
  NextFetchEvent,
  NextMiddleware,
  NextRequest,
  NextResponse,
} from "next/server";

NextFetchEvent

リクエストの種類 (サーバーサイドレンダリング、API エンドポイント、プレビューなど) を表すオブジェクトです。

NextMiddleware

リクエスト処理の途中で実行される関数を表すオブジェクトです。リクエストを処理したり、レスポンスを変更したりできます。

NextRequest

Functions: NextRequest | Next.js

Request - Web API | MDN

NextRequest は Web Request API を拡張したものです。

リクエストに関する情報を表すオブジェクトです。URL、HTTP メソッド、ヘッダーなどの情報が含まれます。

NextResponse

NextResponseはAPIで使用されるレスポンスオブジェクトです。ステータスコード、ヘッダー、ボディなどの情報が含まれます。

基礎知識として、HTMLレスポンスの中身は
ステータスコード
ヘッダー
ボディ
で構成されています。

HTTP/1.1 200 OK       <<<ステータスコード
Content-Type: text/html <<<ヘッダー
Content-Length: 1234

<!DOCTYPE html>           <<<ボディ
<html>
<head>
<title>Hello, world!</title>
</head>
<body>
<h1>Hello, world!</h1>
</body>
</html>

NextResponseは↑このHTMLレスポンスを書き換えたり生成したりするオブジェクトです。

レスポンスに関する情報を表すオブジェクトです。ステータスコード、ヘッダー、ボディなどの情報が含まれます。

NextResponseはClassで、それに複数のインスタンスメソッドが定義されています。

node_modules.pnpm\next@14.0.4_react-dom@18.2.0_react@18.2.0\node_modules\next\dist\server\web\spec-extension\response.d.ts

// NextResponse
export declare class NextResponse<Body = unknown> extends Response {
    [INTERNALS]: {
        cookies: ResponseCookies;
        url?: NextURL;
        body?: Body;
    };
    constructor(body?: BodyInit | null, init?: ResponseInit);
    get cookies(): ResponseCookies;
    static json<JsonBody>(body: JsonBody, init?: ResponseInit): NextResponse<JsonBody>;
    static redirect(url: string | NextURL | URL, init?: number | ResponseInit): NextResponse<unknown>;
    static rewrite(destination: string | NextURL | URL, init?: MiddlewareResponseInit): NextResponse<unknown>;
    static next(init?: MiddlewareResponseInit): NextResponse<unknown>;
}

// 定義されているインスタンスメソッド
cookies
set(name, value)
get(name)
getAll()
delete(name)
json()
redirect()
rewrite()
next()

これらのインスタンスメソッドを使って、レスポンスを書き換えたり、生成できたりします。

国際化

Middlewareでの国際化

Routing: Internationalization | Next.js

middleware.ts
import { match } from '@formatjs/intl-localematcher'
import Negotiator from 'negotiator'

let headers = { 'accept-language': 'en-US,en;q=0.5' }
let languages = new Negotiator({ headers }).languages()
let locales = ['en-US', 'nl-NL', 'nl']
let defaultLocale = 'en-US'

match(languages, locales, defaultLocale) // -> 'en-US'

middleware.ts
import { NextResponse } from "next/server";

let locales = ['en-US', 'nl-NL', 'nl']

// Get the preferred locale, similar to the above or using a library
function getLocale(request) { ... }

export function middleware(request) {
  // Check if there is any supported locale in the pathname
  const { pathname } = request.nextUrl
  const pathnameHasLocale = locales.some(
    (locale) => pathname.startsWith(`/${locale}/`) || pathname === `/${locale}`
  )

  if (pathnameHasLocale) return

  // Redirect if there is no locale
  const locale = getLocale(request)
  request.nextUrl.pathname = `/${locale}${pathname}`
  // e.g. incoming request is /products
  // The new URL is now /en-US/products
  return NextResponse.redirect(request.nextUrl)
}

export const config = {
  matcher: [
    // Skip all internal paths (_next)
    '/((?!_next).*)',
    // Optional: only run on root (/) URL
    // '/'
  ],
}

Next.jsを使った複数言語対応サイトの作成方法

Next.jsを利用すると、複数の言語に対応したサイトを簡単に作れます。サイトをグローバル化させるためには、コンテンツの翻訳(ローカリゼーション)と、言語に応じた適切なページにユーザーを誘導する仕組み(国際化ルーティング)が必要です。

用語

ロケール: 言語や地域の設定情報のこと。
例えば、
"en-US"はアメリカ英語
"nl-NL"はオランダ語(オランダ)
"nl"はオランダ語(地域指定なし)
を表します。

ルーティングの概要

通常、ブラウザが保持しているユーザーの言語設定をもとに、使用するロケールが決定されます。

ユーザーがブラウザで言語設定を変更すると、アプリケーションに送信される Accept-Language ヘッダー情報が更新されます。

このヘッダー情報をもとに、ライブラリを使って適切なロケールを選択することができます。

ルーティングの国際化

Next.jsでは、
サブパス (/en/products) 形式
ドメイン (fr.example.com/products) 形式
のどちらかを使って、ルーティングを国際化することができます。

ミドルウェアと呼ばれるプログラムの中で、ユーザーのロケールに合わせてリダイレクト処理を行うのが一般的です。

コンテンツの管理

国際化するすべてのファイルを app/[lang] ディレクトリ以下に配置します。

これにより、Next.jsのルーターが自動的に異なるロケールに対応したページを処理し、 lang パラメータを各レイアウトやページに渡してくれます。

app/[lang]/page.js
// You now have access to the current locale
// e.g. /en-US/products -> `lang` is "en-US"
export default async function Page({ params: { lang } }) {
  return ...
}




Middlewareの中身

Response

Response - Web API | MDN
https://developer.mozilla.org/ja/docs/Web/API/Response

Response
Response はフェッチ API のインターフェイスで、リクエストのレスポンスを表します。

API
Application Programming Interface
異なるソフトウェアやアプリケーション間で機能を共有するための仕組み

Responseはフェッチ API の一つ

インターフェイス =通信規約

リクエストのレスポンスは
requestを実行すると、Responseの型でオブジェクトのデータが含まれたデータが返ってくる。

Functions: NextResponse | Next.js
https://nextjs.org/docs/app/api-reference/functions/next-response

NextResponseはResponse型の拡張

初期化

let response = NextResponse.next()

responseに値を設定して返す。


41
22
1

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
41
22