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

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

Vercel のエラーログ。本番直後の管理画面で、ローカルでは /dashboard 以下がすべて保護され、未ログインユーザーはログインページへ飛んでいた。ところが本番では /dashboard/settings/profile に直接アクセスすると認証をすり抜け、機密データが見えてしまった。

すぐコードを開いた。Middleware ファイルもあり、ロジックも問題なさそうだった。原因は?

Next.js ドキュメントの matcher 節を読み返し、ようやく判明した。/dashboard/:path は 1 階層しかマッチせず、多階層パスは対象外だった。正しくは /dashboard/:path*。小さなアスタリスクの有無が、本番障害の原因だった。

Middleware では Edge Runtime の制限、matcher の不具合、無限リダイレクトなど、私も何度も踏んできた。

認証、国際化、A/B テストを Middleware で実装するなら、陥りやすい罠と設定ルール、3 つの完全な実装例をこの記事にまとめた。前置きは省き、書き方・回避策・動かし方に絞る。

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 });

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

もう少し実用的な例:カスタム Header の追加

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

export function middleware(request: NextRequest) {
  const response = NextResponse.next();

  // すべてのレスポンスにカスタム header を付与
  response.headers.set('x-custom-header', 'my-value');

  return response;
}

タイムスタンプやリクエスト元のマーキングなど、全レスポンスに共通の header を載せたいときに使えます。

バージョンについて(重要)

Next.js 15 では公式に middleware.tsproxy.ts にリネームされました。middleware.ts も下位互換で動きますが、新規プロジェクトは新名称を推奨します。本文のコードは Next.js 14/15 向けで、概念と API は共通です。

パスマッチング(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除く すべてのパスにマッチ」という意味です。
正直複雑なので、公式サンプルのコピペで十分です。

もう一つの罠:動的な値は使えない

const lang = 'zh'; // 変数
export const config = {
  matcher: [`/${lang}/:path*`] // ❌ 不可
}

matcher は静的で、ビルド時に確定できる値だけです。テンプレート文字列で変数を埋め込んだり、実行時に生成したりはできません。

動的判定は middleware 関数内で行います:

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

  if (pathname.startsWith('/zh') || pathname.startsWith('/en')) {
    // 処理
  }

  return NextResponse.next();
}

export const config = {
  matcher: ['/:locale/:path*']
}

おすすめ matcher テンプレート(そのまま使える)

特定ルートの保護(管理画面など):

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

全ルートだが静的リソースは除外:

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

すべての API ルートを保護:

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

matcher の公式ドキュメントは簡潔なので、上記の表とテンプレートで足りることが多いです。

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 {
    const secret = new TextEncoder().encode(process.env.JWT_SECRET);
    const { payload } = await jwtVerify(token, secret);

    const response = NextResponse.next();
    response.headers.set('x-user-id', payload.userId as string);

    return response;
  } catch (error) {
    const response = NextResponse.redirect(new URL('/login', request.url));
    response.cookies.delete('auth-token');
    return response;
  }
}

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

ポイント

  • jsonwebtoken ではなく jose を使う(後者は Node.js crypto に依存)
  • JWT シークレットは process.env から(Edge Runtime でも利用可)
  • 検証失敗時は cookie を削除し、繰り返し失敗を防ぐ

どうしても DB が必要なとき

ユーザー停止状態の確認など、DB 照会が必要な場合は Middleware から API ルートを呼びます:

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

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

  const apiUrl = new URL('/api/check-user-status', request.url);
  const response = await fetch(apiUrl, {
    headers: { 'x-user-id': userId }
  });

  const { isActive } = await response.json();

  if (!isActive) {
    return NextResponse.redirect(new URL('/account-suspended', request.url));
  }

  return NextResponse.next();
}

API ルートは Node.js ランタイムで DB に自由にアクセスできます。ただしレイテンシは増えるので、必要なときだけ使いましょう。

Web Crypto API について

暗号化が必要ならブラウザ標準の Web Crypto API を使います:

const data = new TextEncoder().encode('hello world');
const hashBuffer = await crypto.subtle.digest('SHA-256', data);
const hashArray = Array.from(new Uint8Array(hashBuffer));
const hashHex = hashArray.map(b => b.toString(16).padStart(2, '0')).join('');

Node.js の crypto より冗長ですが、Edge ではこれが正攻法です。

ライブラリが Edge 対応かどうか

ドキュメントを確認するか、実行してみる。Native Node.js APIs are not supported が出たら非対応です。

よく使う Edge 向けの例:

  • JWT:josejsonwebtoken は不可)
  • DB:Vercel Postgres、Supabase、Prisma(一部)
  • ログ:Logtail、Axiom

Middleware は「軽く、速く」が鉄則。重い処理は API ルートへ。

実践ケース:3 つのコアシナリオ

理論のあとはコード。以下は実プロジェクトで使った 3 パターンです。そのままコピーして動かせます。

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

シナリオ:管理画面の /dashboard 以下をすべてログイン必須にする。

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

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

  if (!token) {
    const loginUrl = new URL('/login', request.url);
    loginUrl.searchParams.set('from', pathname);
    return NextResponse.redirect(loginUrl);
  }

  try {
    const secret = new TextEncoder().encode(process.env.JWT_SECRET);
    const { payload } = await jwtVerify(token, secret);

    const expiresAt = payload.exp as number;
    const now = Math.floor(Date.now() / 1000);
    const shouldRefresh = expiresAt - now < 3600;

    const response = NextResponse.next();

    if (shouldRefresh) {
      response.headers.set('x-token-refresh-needed', 'true');
    }

    response.headers.set('x-user-id', payload.userId as string);
    response.headers.set('x-user-role', payload.role as string);

    return response;
  } catch (error) {
    const loginUrl = new URL('/login', request.url);
    loginUrl.searchParams.set('from', pathname);
    loginUrl.searchParams.set('reason', 'expired');

    const response = NextResponse.redirect(loginUrl);
    response.cookies.delete('auth-token');

    return response;
  }
}

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

ポイント

  1. from でログイン後の戻り先を保持
  2. 有効期限が近い token は更新フラグを header に載せる
  3. ユーザー情報を header で下流に渡せる(任意)

テスト:cookie を消して /dashboard/login?from=/dashboard へ。ログイン後に cookie を設定して再アクセス → 正常表示。

よくある問題:ローカルは動くが本番で動かない → JWT_SECRET が本番環境変数に未設定。

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

シナリオ/ アクセス時に言語設定に応じて /zh または /en へ振り分ける。

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

const supportedLocales = ['en', 'zh', 'ja'];
const defaultLocale = 'en';

function getPreferredLocale(request: NextRequest): string {
  const urlLocale = request.nextUrl.searchParams.get('lang');
  if (urlLocale && supportedLocales.includes(urlLocale)) {
    return urlLocale;
  }

  const cookieLocale = request.cookies.get('NEXT_LOCALE')?.value;
  if (cookieLocale && supportedLocales.includes(cookieLocale)) {
    return cookieLocale;
  }

  const acceptLanguage = request.headers.get('accept-language');
  if (acceptLanguage) {
    const browserLang = acceptLanguage.split(',')[0].split('-')[0];
    if (supportedLocales.includes(browserLang)) {
      return browserLang;
    }
  }

  return defaultLocale;
}

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

  const pathnameHasLocale = supportedLocales.some(
    locale => pathname.startsWith(`/${locale}/`) || pathname === `/${locale}`
  );

  if (!pathnameHasLocale) {
    const locale = getPreferredLocale(request);
    const newUrl = new URL(`/${locale}${pathname}`, request.url);
    newUrl.search = request.nextUrl.search;

    const response = NextResponse.redirect(newUrl);
    response.cookies.set('NEXT_LOCALE', locale, {
      maxAge: 60 * 60 * 24 * 30,
      path: '/'
    });

    return response;
  }

  return NextResponse.next();
}

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

ポイント:URL パラメータ > Cookie > Accept-Language の優先順位。Cookie で次回以降の選択を記憶。matcher で静的リソースを除外。

next-intl 連携

import { createI18nMiddleware } from 'next-intl/middleware';

export default createI18nMiddleware({
  locales: ['en', 'zh', 'ja'],
  defaultLocale: 'en'
});

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

ケース 3:A/B テストとフィーチャーフラグ

シナリオ:トップページを 50% のユーザーに新版(/homepage-v2)で見せ、URL は / のまま。

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

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

  if (pathname !== '/') {
    return NextResponse.next();
  }

  let variant = request.cookies.get('ab-test-homepage')?.value;

  if (!variant) {
    variant = Math.random() < 0.5 ? 'A' : 'B';
  }

  let response: NextResponse;

  if (variant === 'B') {
    response = NextResponse.rewrite(new URL('/homepage-v2', request.url));
  } else {
    response = NextResponse.next();
  }

  response.cookies.set('ab-test-homepage', variant, {
    maxAge: 60 * 60 * 24 * 7,
    path: '/'
  });

  response.headers.set('x-ab-variant', variant);

  return response;
}

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

ポイントrewrite で URL を変えずに内容だけ切替。Cookie でグループ固定。x-ab-variant で分析用に識別。

ページ側の計測例

// app/page.tsx
import { headers } from 'next/headers';

export default function HomePage() {
  const headersList = headers();
  const abVariant = headersList.get('x-ab-variant');

  useEffect(() => {
    analytics.track('page_view', {
      page: 'homepage',
      variant: abVariant
    });
  }, [abVariant]);

  return <div>...</div>;
}

応用:ユーザー ID で安定グループ分け

const userId = request.cookies.get('user-id')?.value;

if (userId) {
  const hash = simpleHash(userId);
  variant = hash % 2 === 0 ? 'A' : 'B';
} else {
  variant = request.cookies.get('ab-test-homepage')?.value ||
            (Math.random() < 0.5 ? 'A' : 'B');
}

function simpleHash(str: string): number {
  let hash = 0;
  for (let i = 0; i < str.length; i++) {
    hash = ((hash << 5) - hash) + str.charCodeAt(i);
    hash |= 0;
  }
  return Math.abs(hash);
}

認証と i18n を組み合わせることも多いです。

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

Middleware は書きやすいが、運用で差がつきます。

モジュール分割

プロジェクトが大きくなると 1 ファイルに詰め込むと破綻します。Next.js は middleware.ts を 1 つだけ許可しますが、ロジックは別ファイルに分けられます。

プロジェクトルート/
├── middleware/
│   ├── auth.ts
│   ├── i18n.ts
│   ├── ab-test.ts
│   └── rate-limit.ts
├── middleware.ts
// middleware.ts
import { NextRequest, NextResponse } from 'next/server';
import { checkAuth } from './middleware/auth';
import { handleI18n } from './middleware/i18n';
import { handleABTest } from './middleware/ab-test';

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

  const i18nResponse = handleI18n(request);
  if (i18nResponse) return i18nResponse;

  if (pathname.startsWith('/dashboard')) {
    const authResponse = await checkAuth(request);
    if (authResponse) return authResponse;
  }

  if (pathname === '/') {
    return handleABTest(request);
  }

  return NextResponse.next();
}

export const config = {
  matcher: ['/((?!api|_next/static|_next/image|favicon.ico).*)']
}
// middleware/auth.ts
import { NextRequest, NextResponse } from 'next/server';
import { jwtVerify } from 'jose';

export async function checkAuth(request: NextRequest): Promise<NextResponse | null> {
  const token = request.cookies.get('auth-token')?.value;

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

  try {
    const secret = new TextEncoder().encode(process.env.JWT_SECRET);
    await jwtVerify(token, secret);
    return null;
  } catch {
    return NextResponse.redirect(new URL('/login', request.url));
  }
}

キャッシュ:重複計算を減らす

有効な token は毎回フル検証しなくてよい場合もあります。設定は Vercel Edge Config などに置けます:

import { get } from '@vercel/edge-config';

export async function middleware(request: NextRequest) {
  const featureFlags = await get('feature-flags');

  if (featureFlags?.newDashboard) {
    return NextResponse.rewrite(new URL('/dashboard-v2', request.url));
  }

  return NextResponse.next();
}

Header サイズに注意

Middleware で付けた header はレスポンスに載ります。多すぎると 431 Request Header Fields Too Large の原因になります。

  • 合計 8KB 以内を目安
  • 必要最小限だけ渡す
  • 大きなオブジェクトは token にまとめる
// ❌ 避ける
response.headers.set('x-user-data', JSON.stringify(userData));

// ✅ 推奨
response.headers.set('x-user-id', user.id);
response.headers.set('x-user-role', user.role);

matcher は狭く

// あまり良くない
matcher: ['/:path*']

// より良い
matcher: ['/dashboard/:path*', '/api/:path*']

必要なルートだけに絞ると実行回数が減ります。

監視とデバッグ

開発時:

export function middleware(request: NextRequest) {
  if (process.env.NODE_ENV === 'development') {
    console.log('Middleware 実行:', {
      path: request.nextUrl.pathname,
      method: request.method,
      cookies: request.cookies.getAll()
    });
  }
  // ...
}

本番はログを出しすぎない。エラー時だけ Logtail などへ送るのが無難です。

ベストプラクティス一覧

✅ すべきこと

  • Middleware は軽量に、複雑処理は API ルートへ
  • matcher で対象パスを絞る
  • 認証は JWT で DB 照会を避ける
  • 機能ごとにファイル分割
  • 開発環境でデバッグログ
  • Cookie の有効期限を適切に

❌ 避けること

  • Middleware で重い計算や DB 直叩き
  • matcher 未設定で全リクエスト実行
  • Node.js API 依存ライブラリの使用
  • 8KB 超の header
  • 本番で大量ログ
  • 無限リダイレクトループ

よくあるエラーとデバッグ

最後に、私が何時間も潰したエラーと対処法です。

エラー 1:Middleware が実行されない

症状:書いたのに効いていない。

原因確認対処
ファイル位置ルートまたは src 直下か移動
matcher 漏れpathname をログmatcher 修正
構文エラーコンソール修正
export 漏れexport function middleware修正
キャッシュ.next 削除rm -rf .next && npm run dev
export function middleware(request: NextRequest) {
  console.log('🔥 Middleware 実行:', request.nextUrl.pathname);
  // ...
}

ログが出なければ上表を順に確認。

エラー 2:Native Node.js APIs are not supported in the Edge Runtime

スタックトレースで原因ライブラリを特定。よくあるのは fsjsonwebtoken、MongoDB/MySQL ドライバ。

// ❌
import jwt from 'jsonwebtoken';

// ✅
import { jwtVerify } from 'jose';

エラー 3:Invalid middleware found

  • matcher: [] は不可
  • export function middleware が必須
  • 必ず NextResponse を return

エラー 4:無限リダイレクト

/login も matcher に含めるとループします。公開ページを除外するか、matcher を /dashboard/:path* だけに絞ります。

エラー 5:環境変数が undefined

.env.local とデプロイ先の環境変数を確認。変更後は dev サーバーを再起動。

デバッグまとめ

  1. x-middleware-executed などの header で通過確認
  2. 処理を段階的にコメントアウトして切り分け
  3. Vercel の Edge Function Logs
  4. npm run dev -- --turbo(Next.js 15+)

まとめ

三つに絞ると次のとおりです。

1. Middleware の境界をはっきりさせる

認証、リダイレクト、A/B テストなど軽い傍受向き。DB や重い計算は API ルート・Server Component へ。門番であって執事ではない。

2. matcher が最重要

動的ルートは *、静的は除外、ログインページは傍受しない。迷ったらテンプレをコピーし、開発環境でログを見る。

3. Edge Runtime の制限を受け入れる

完全な Node.js ではない。JWT、Web Crypto、API ルート呼び出しで回避する。

matcher、Edge 制限、リダイレクトループ——私が踏んだ道を短くする記事にした。グローバル傍受が必要なら Middleware を試し、詰まったらエラー章に戻ってください。

次回は Server Actions も坑が多いので、また整理する予定です。Middleware が一度通れば、それで十分です。

Next.js Middleware 設定の完全フロー

Middleware ファイルの作成から、ルート保護・国際化・A/B テストの 3 シナリオ実装まで

⏱️ 目安時間: 3 時間

  1. 1

    ステップ1: Middleware ファイルを作成する

    作成手順:
    • 配置:middleware.ts(プロジェクトルート)
    • config オブジェクトで matcher をエクスポート
    • middleware 関数でリクエストを処理

    基本構造:
    export const config = {
    matcher: '/dashboard/:path*'
    }

    export function middleware(request: NextRequest) {
    // 処理ロジック
    }
  2. 2

    ステップ2: matcher ルールを設定する

    マッチ規則:
    • 単一パス:'/dashboard'
    • 動的ルート:'/dashboard/:path*'(* で多階層にマッチ)
    • 複数パス:['/dashboard/:path*', '/admin/:path*']
    • 除外:否定先読み '/((?!api|_next/static|_next/image|favicon.ico).*)'

    注意:
    • 動的ルートは * 必須
    • 静的リソース(_next/static 等)は除外
    • ログインページなど公開ページは傍受しない
  3. 3

    ステップ3: ルート保護(認証)を実装する

    手順:
    1. cookie から token を読む
    2. token を検証(JWT または API 呼び出し)
    3. 未ログインはログインページへリダイレクト
    4. ログイン済みはそのまま通過

    要点:
    • NextRequest.cookies で cookie 取得
    • NextResponse.redirect でリダイレクト
    • NextResponse.next で続行
    • 無限リダイレクトループに注意
  4. 4

    ステップ4: 国際化(言語切替)を実装する

    手順:
    1. 言語設定を検出(cookie、header、デフォルト)
    2. パスからリダイレクト要否を判断
    3. URL に言語プレフィックスを付与
    4. 言語 cookie を設定

    要点:
    • request.headers.get('accept-language')
    • request.nextUrl.pathname でパス取得
    • NextResponse.rewrite で URL 書き換え
    • URL 構造を変えずに言語切替も可能
  5. 5

    ステップ5: Edge Runtime 制限への対処

    制限と対策:
    • Node.js API 非対応 → Web 標準 API を使う
    • ファイルシステム不可 → 環境変数または API 呼び出し
    • 一部 npm 非対応 → Edge 対応を確認
    • JWT 検証 → Web Crypto API または API ルート

    デバッグ:
    • console.log でログ出力
    • Vercel Edge Functions ログを確認
    • try-catch でエラー捕捉
  6. 6

    ステップ6: テストとデバッグ

    テスト項目:
    • マッチ対象の全パス
    • 非マッチパス(傍受されないこと)
    • リダイレクトロジック
    • Edge Runtime 互換性

    デバッグ方法:
    • middleware に console.log
    • ブラウザの Network タブ
    • Vercel Edge Functions ログ
    • Next.js 開発モードの警告

FAQ

Middleware の matcher 設定が効かないときは?
確認項目:
1) matcher パスが正しいか(動的ルートには * が必要)
2) 静的リソースを除外しているか
3) パス形式が Next.js 対応か(任意の正規表現ではなく公式形式)

middleware に console.log を入れ、どのパスがマッチしたか確認してください。
多階層パスがマッチしないのはなぜ?
動的ルートは `:path*`(アスタリスク必須)でないと多階層にマッチしません。`/dashboard/:path*` は `/dashboard/settings/profile` にマッチしますが、`/dashboard/:path` は `/dashboard/settings` の 1 階層までです。
Edge Runtime でライブラリが使えないときは?
Edge Runtime は完全な Node.js ではなく、すべての npm パッケージに対応しません。

対策:
1) パッケージの Edge 対応を確認
2) Web 標準 API で代替
3) 複雑な処理は API ルートまたは Server Component へ
4) Edge 対応の代替ライブラリを使う
無限リダイレクトループを防ぐには?
リダイレクト先が matcher の対象外であることを確認してください。例:`/login` へ飛ばすなら matcher から `/login` を除外。否定先読み例:`'/((?!login|api).*)'`。
Middleware でデータベースに接続できますか?
非推奨です。Middleware は Edge Runtime で軽量に保つべきです。

DB が必要なら:
1) Middleware から API ルートを呼ぶ
2) 環境変数で設定を保持
3) 複雑な処理は API ルートまたは Server Component へ
Middleware のデバッグ方法は?
方法:
1) middleware 関数内で console.log
2) ブラウザ Network タブでリクエスト・レスポンス確認
3) Vercel Edge Functions ログ
4) Next.js 開発モードの警告・エラー
Middleware と API ルートの違いは?
Middleware:
• Edge Runtime で実行
• ページや API 到達前に実行
• 軽量な傍受・転送向き

API ルート:
• Node.js ランタイム
• 完全な Node.js API と DB にアクセス可能
• 複雑なビジネスロジック向き

7分で読めます · 公開日: 2025年12月25日 · 更新日: 2026年6月8日

関連記事

コメント

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