言語を切り替える
テーマを切り替える

Next.js Middleware 実践ガイド:パス一致、Edge Runtime 制限とよくある落とし穴

深夜2時、私は Vercel のエラーログを呆然と見つめていました。

ローカルでのテストでは、/dashboard 以下のすべてのルートが正しく保護され、未ログインユーザーはログインページにリダイレクトされていました。しかし、本番環境にデプロイしてみると、ユーザーが /dashboard/settings/profile に直接アクセスすると、なんと認証をスルーして機密データが見えてしまっていたのです。

コードをすぐに確認しましたが、Middleware ファイルはそこにあり、ロジックにも問題はないように見えました。一体何が原因なのか?

Next.js のドキュメントを30分ほど読み漁った末、ようやく matcher 設定の小さなセクションに答えを見つけました。私は /dashboard/:path と書いていましたが、これでは /dashboard/settings のような1階層下のパスにしかマッチしません。階層が深いパスも含めるには、正しい書き方は /dashboard/:path* でした。あの小さなアスタリスク一つに、危うく責任を負わされるところでした。

正直なところ、Middleware でつまずいたのはこれが初めてではありません。Edge Runtime が特定のライブラリをサポートしていない、matcher 設定が効かない、無限リダイレクトループ… これらの落とし穴には、ほぼすべてハマったことがあります。

もしあなたが Next.js Middleware を使っている、あるいはこれから認証、国際化、A/Bテストなどに使おうとしているなら、この記事がきっと役に立つはずです。陥りやすい罠、難解な設定ルール、そして3つの完全な実践ケーススタディを分解して解説します。

「現代のWeb開発において…」といった決まり文句は省略します。実際の問題、書き方、回避策、そして Middleware を本当に機能させる方法について話しましょう。

Middleware とは何か?なぜ使うのか?

簡単に言えば、Middleware は「検問所」です。

ユーザーのリクエストがページや API に到達する前に、この検問所を通過します。ここでユーザーの身元確認、リクエスト内容の変更、さらにはレスポンスの直接返却(例:未ログインユーザーをログインページへ飛ばす、地域に応じて別言語版へジャンプさせる)などが可能です。

重要なのは、これが Edge Runtime で動作するという点です。サーバー上ではなく、ユーザーに近いエッジノード(CDN)で実行されます。距離が近いため遅延が少なく、コールドスタートもほぼゼロです。つまり、Middleware はユーザーに最も近いコードの層と言えます。

Edge Runtime と Node.js Runtime の違い

特性Edge RuntimeNode.js Runtime
起動速度ほぼゼロ(ゼロコールドスタート)数百ミリ秒かかる場合がある
実行場所グローバルなエッジノード特定のリージョンのサーバー
APIサポートWeb 標準 API完全な Node.js API
適用シナリオ軽量ロジック、高速レスポンス複雑な計算、DB操作

簡単に言うと「速いが、能力は限定的」です。Node.js のモジュール(fs, path など)は使えませんし、ほとんどのデータベースにも直接接続できません。これが後々、最もハマりやすいポイントになります。

Middleware を使うべき場面

すべてのロジックを Middleware に詰め込むべきではありません。最も一般的なユースケースをまとめました:

  1. 認証(Auth Gate)
    最も古典的な用途です。ログイン状態をチェックし、未認証ならリダイレクトします。リクエストがサーバーに到達する前に処理するため、Server Component で判定するより高速です。

  2. 国際化(i18n)
    ユーザーの言語設定(URL、Cookie、ブラウザ設定など)に基づいて、適切な言語バージョンへ自動転送します(//ja/en)。

  3. A/B テスト
    ユーザーをランダムにグループ分けし、異なるバージョンのページを表示します。Cookie でグループを維持し、リロードしても同じバージョンが見えるようにします。

  4. Bot 検出とレート制限
    クローラーや悪意あるリクエストをブロックしたり、特定の IP からのアクセス頻度を制限したりします。

  5. ログ記録と分析
    リクエストの基本情報(パス、参照元、UA)を記録し、分析サービスへ送信します。

  6. URL 書き換え(Rewrite)
    ブラウザのアドレスバーを変えずに、内部的に別のパスへマッピングします。動的ルーティングや A/B テストで特に役立ちます。

なぜページコンポーネントで直接やらないのか?

できなくはありませんが、遅くなります。Server Component や Client Component のロジックは、リクエストがサーバーに到達した後、あるいはページレンダリング後に実行されます。Middleware はエッジで即座に遮断できるため、レスポンスが速く、ユーザー体験が向上します。

また、管理の一元化も理由の一つです。保護が必要な全ページに個別に認証ロジックを書くのは現実的ではありません。Middleware なら一箇所で済みます。

ただし、複雑なビジネスロジック、DBクエリ、重い計算処理は API ルートや Server Component に任せるべきです。Middleware は軽量かつ高速に保ちましょう。

Middleware の基本設定とファイル構造

ファイルの場所

Next.js は Middleware の配置場所に厳格です。プロジェクトのルートディレクトリ、または src ディレクトリ直下に、middleware.ts(または .js)という名前で配置する必要があります。

プロジェクトルート/
├── app/
├── middleware.ts    ← ここ
├── package.json

または src を使っている場合:

プロジェクトルート/
├── src/
│   ├── app/
│   ├── middleware.ts    ← ここ
├── package.json

注意:プロジェクトに middleware.ts は1つしか置けません。app ディレクトリ内などに複数作ることはできません。これは Middleware がグローバルな「門番」であるという設計思想によるものです。

最もシンプルな Middleware

import { NextRequest, NextResponse } from 'next/server';

export function middleware(request: NextRequest) {
  console.log('リクエスト受信:', request.url);
  return NextResponse.next(); // 通過させる
}

これだけです。NextResponse.next() は「問題なし、次へ進め」という意味で、リクエストは正常にページや API に到達します。

コア API:NextRequest と NextResponse

NextRequest は標準の Web Request の拡張版で、便利なプロパティがあります:

  • request.nextUrl: 解析済みの URL オブジェクト(pathname, search などに直接アクセス可)
  • request.cookies: Cookie の読み書きが簡単
  • request.geo: ユーザーの地理情報(Vercel などの対応プラットフォームが必要)

NextResponse はいくつかの応答方法を提供します:

  1. 通過(そのまま続行)

    return NextResponse.next();
  2. リダイレクト(新しい URL へ移動)

    return NextResponse.redirect(new URL('/login', request.url));

    ユーザーのアドレスバーが変わります。

  3. 書き換え(内部リダイレクト、URL は不変)

    return NextResponse.rewrite(new URL('/dashboard/v2', request.url));

    ユーザーは /dashboard にアクセスしているつもりですが、実際には /dashboard/v2 の内容が表示されます。A/B テストやバージョン切り替えに便利です。

  4. レスポンスを直接返す

    return new NextResponse('アクセス拒否', { status: 403 });

    処理を中断し、ユーザーに直接コンテンツを返します。

パスマッチング(Matcher):最大の落とし穴

正直なところ、matcher で一番多くの失敗をしました。

なぜ matcher が必要なのか?

matcher を設定しないと、Middleware は すべてのリクエスト に対して実行されます。これには CSS、JS、画像、フォントなどの静的リソースも含まれます。ページの読み込みでブラウザが20個の静的ファイルをリクエストすれば、Middleware も20回実行されます。リソースの無駄ですし、レスポンスも遅くなります。

matcher の役割は、Next.js に「これらのパスでのみ Middleware を実行せよ」と伝えることです。

基本構文

middleware.ts ファイルから config オブジェクトをエクスポートします:

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

これは、「/dashboard および /api で始まるパスでのみ実行する」という意味になります。

修飾子:*+? の意味

これらの記号はパスマッチの「貪欲さ」を制御します:

*(0個以上)
/dashboard/:path* は以下にマッチします:

  • /dashboard
  • /dashboard/settings
  • /dashboard/settings/profile

+(1個以上)
/dashboard/:path+ は以下にマッチします:

  • /dashboard
  • /dashboard/settings
  • /dashboard/settings/profile

?(0個または1個)
/dashboard/:path? は以下にマッチします:

  • /dashboard
  • /dashboard/settings
  • /dashboard/settings/profile

ほとんどの場合、* を使っておけば間違いありません。

よくある間違いと解決策(重要!)

この表は私の血と涙の結晶です。保存推奨です。

問題間違った書き方正しい書き方理由
動的ルートのサブパス漏れ/dashboard/:path/dashboard/:path** がないと1階層目しかマッチせず、深い階層が無視される
ルートパス漏れmatcher: ['/dashboard/:path*']matcher: ['/', '/dashboard/:path*']ルートパス / は自動的には含まれないため、明示が必要
静的リソース巻き込みmatcher: ['/:path*']matcher: ['/((?!_next|favicon.ico).*)']正規表現で _next などの内部パスを除外する必要がある
API ルート漏れmatcher: ['/api/users']matcher: ['/api/:path*']具体的なパスだけだと他が漏れる。ワイルドカードで全 API をカバー

静的リソースの罠

以下のような matcher を書いたとします:

export const config = {
  matcher: ['/:path*'] // 全パスにマッチさせたい
}

結果、Next.js 内部の _next/static パスまでマッチしてしまい、Middleware が暴走してページ読み込みが激重になります。

正しいやり方:否定先読みで除外

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

この正規表現は「api, _next/static, _next/image, favicon.ico除く すべてのパスにマッチ」という意味です。
正直複雑なので、公式サンプルのコピペで十分です。

Edge Runtime の制限と回避策

この章の内容を知らずに、私は大ハマりしました。

Middleware でユーザー認証をしようと思い、DBにクエリを投げてトークンを確認するコードを書きました。ローカルで実行すると即エラー:Native Node.js APIs are not supported in the Edge Runtime

「ただDBに繋ぎたいだけなのに、なぜダメなんだ?」

後に理解したのは、Edge Runtime は完全な Node.js 環境ではなく、多くの一般的な API やライブラリが使えないということです。

Edge Runtime でサポートされていないもの

カテゴリ非対応 API/モジュール影響
ファイルシステムfs, pathローカルファイルの読み書き不可
子プロセスchild_process外部コマンド実行不可
暗号化一部の crypto APIWeb Crypto API で代用が必要
データベースMongoDB, MySQL ネイティブドライバ多くの従来のDBドライバが使用不可
その他process.emit, setImmediate一部の低レベル Node.js API

実際のところ、何が困る?

  1. DBでユーザー認証ができない(直接クエリできない)
  2. 設定ファイルを読めないconfig.json 等を fs で読めない)
  3. Node.js API 依存のライブラリが使えない

クリエイティブな回避策

重要な考え方:Middleware は「軽量な前哨基地」として使い、重い処理は後ろに任せる。

要件❌ Edge Runtime 制限✅ 解決策
ユーザー認証DB接続不可JWT でステートレス検証、または API ルートへフェッチ
暗号化crypto 一部不可Web Crypto API を使用
設定読込ファイルアクセス不可環境変数(process.env)または API 経由
ログ記録ファイル書込不可HTTP経由で外部ログサービス(Logtail等)へ送信
DB操作ドライバ非対応Edge 対応 DB(Vercel Postgres, Supabase)を使用

実践例:JWT 検証(推奨)

JWT (JSON Web Token) は Middleware に最適な認証方式です。トークン自体に情報が含まれているため、DBアクセスが不要(ステートレス)だからです。

import { NextRequest, NextResponse } from 'next/server';
import { jwtVerify } from 'jose'; // Edge Runtime 対応のライブラリ

export async function middleware(request: NextRequest) {
  const token = request.cookies.get('auth-token')?.value;

  if (!token) {
    return NextResponse.redirect(new URL('/login', request.url));
  }

  try {
    // JWT 検証(jose を使用)
    const secret = new TextEncoder().encode(process.env.JWT_SECRET);
    const { payload } = await jwtVerify(token, secret);

    // 検証成功
    return NextResponse.next();
  } catch (error) {
    // トークン無効
    const response = NextResponse.redirect(new URL('/login', request.url));
    response.cookies.delete('auth-token');
    return response;
  }
}

ポイント:

  • jsonwebtoken ではなく jose を使う(前者は Node.js の crypto モジュールに依存しており Edge で動かないことが多い)。
  • JWT シークレットは環境変数から読み込む。

実践ケーススタディ

ケース1:認証とルート保護

シナリオ:バックエンド管理システムで、/dashboard 以下の全ページをログイン必須にする。

(※コードは上記のJWT検証例をベースに、リダイレクト後の戻り先 from パラメータなどを追加して実装します。)

ケース2:国際化(i18n)リダイレクト

シナリオ:ユーザーの言語設定(URL、Cookie、ブラウザ)に基づいて //ja/en に振り分ける。

import { NextRequest, NextResponse } from 'next/server';
const supportedLocales = ['en', 'ja', 'zh'];
const defaultLocale = 'ja';

export function middleware(request: NextRequest) {
  const { pathname } = request.nextUrl;

  // パスに既に言語が含まれているか確認
  const pathnameHasLocale = supportedLocales.some(
    locale => pathname.startsWith(`/${locale}/`) || pathname === `/${locale}`
  );

  if (pathnameHasLocale) return NextResponse.next();

  // 言語検出ロジック(簡略版)
  // 実際は Cookie や Accept-Language ヘッダーを見る
  const locale = defaultLocale;

  // リダイレクト
  request.nextUrl.pathname = `/${locale}${pathname}`;
  return NextResponse.redirect(request.nextUrl);
}

export const config = {
  matcher: ['/((?!api|_next/static|_next/image|favicon.ico).*)']
};

ケース3:A/B テスト

シナリオ:トップページのデザイン変更をテストするため、ユーザーの50%を新バージョン(内部パス /homepage-v2)に振り分ける。URLは / のままにする。

import { NextRequest, NextResponse } from 'next/server';

export function middleware(request: NextRequest) {
  const { pathname } = request.nextUrl;
  
  if (pathname !== '/') return NextResponse.next();

  // Cookie でグループを確認
  let bucket = request.cookies.get('ab-test-bucket')?.value;

  // 新規ユーザーならランダムに割り当て
  if (!bucket) {
    bucket = Math.random() < 0.5 ? 'a' : 'b';
  }

  // Bグループなら新ページへ Rewrite(URLは変わらない)
  const response = bucket === 'b' 
    ? NextResponse.rewrite(new URL('/homepage-v2', request.url))
    : NextResponse.next();

  // Cookie に保存
  response.cookies.set('ab-test-bucket', bucket);
  
  return response;
}

パフォーマンス最適化とベストプラクティス

  1. matcher を正確に設定する:不要なパス(特に静的ファイル)で Middleware を走らせないことが、パフォーマンス向上の第一歩です。
  2. ロジックを軽量に保つ:Edge での処理時間は短いほど良いです。重い処理は API ルートに任せましょう。
  3. 条件分岐を早めにする:不要な処理は最初の一行で弾くようにします。
  4. ヘッダーを活用する:Middleware で計算した結果(ユーザーIDなど)を x-user-id のようなカスタムヘッダーに入れて渡すと、後続の処理で再計算せずに済みます。

まとめ

Next.js Middleware は強力なツールですが、使い方を誤ると落とし穴だらけです。

  • matcher の設定ミスは致命的(深層パスの漏れ、静的リソースの巻き込み)。
  • Edge Runtime の制限(Node.js API 不可)を理解し、回避策(JWT, Web Crypto API)を用意する。
  • 認証、i18n、A/B テストなど、軽量かつ高速な「前処理」に特化して使う。

これらを押さえれば、あなたのアプリケーションはより安全に、より高速に動作するはずです。

Next.js Middleware 設定の基本フロー

Middleware ファイルの作成から matcher 設定、レスポンス制御までの手順

⏱️ Estimated time: 15 min

  1. 1

    Step1: ファイルの作成

    プロジェクトルートに `middleware.ts` を作成します。
  2. 2

    Step2: ハンドラー関数の定義

    `middleware` 関数をエクスポートし、`NextRequest` を受け取ります。
  3. 3

    Step3: ロジックの実装

    リクエストを検査し、`NextResponse.next()`, `redirect()`, `rewrite()` のいずれかを返します。
  4. 4

    Step4: Matcher の設定

    `config` オブジェクトをエクスポートし、Middleware を適用するパスを制限します。

FAQ

Middleware でデータベースに接続できますか?
一般的な Node.js 用の DB ドライバは使えません。Vercel Postgres や Supabase など、Edge Runtime に対応したクライアントライブラリか、HTTP 経由でアクセスできるデータベースを使用する必要があります。
複数の Middleware ファイルを作成できますか?
いいえ、Next.js では `middleware.ts` は1つしかサポートされていません。ロジックを分割したい場合は、関数やモジュールとして別ファイルに切り出し、`middleware.ts` からインポートして使用してください。
matcher で正規表現は使えますか?
はい、使えます。特に特定のパスを除外する(例:`!api`)場合に肯定的先読み・否定的先読みなどの正規表現テクニックがよく使われます。

6 min read · 公開日: 2025年12月25日 · 更新日: 2026年1月22日

コメント

GitHubアカウントでログインしてコメントできます

関連記事