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 Runtime | Node.js Runtime |
|---|---|---|
| 起動速度 | ほぼゼロ(ゼロコールドスタート) | 数百ミリ秒かかる場合がある |
| 実行場所 | グローバルなエッジノード | 特定のリージョンのサーバー |
| APIサポート | Web 標準 API | 完全な Node.js API |
| 適用シナリオ | 軽量ロジック、高速レスポンス | 複雑な計算、DB操作 |
簡単に言うと「速いが、能力は限定的」です。Node.js のモジュール(fs, path など)は使えませんし、ほとんどのデータベースにも直接接続できません。これが後々、最もハマりやすいポイントになります。
Middleware を使うべき場面
すべてのロジックを Middleware に詰め込むべきではありません。最も一般的なユースケースをまとめました:
認証(Auth Gate)
最も古典的な用途です。ログイン状態をチェックし、未認証ならリダイレクトします。リクエストがサーバーに到達する前に処理するため、Server Component で判定するより高速です。国際化(i18n)
ユーザーの言語設定(URL、Cookie、ブラウザ設定など)に基づいて、適切な言語バージョンへ自動転送します(/→/jaや/en)。A/B テスト
ユーザーをランダムにグループ分けし、異なるバージョンのページを表示します。Cookie でグループを維持し、リロードしても同じバージョンが見えるようにします。Bot 検出とレート制限
クローラーや悪意あるリクエストをブロックしたり、特定の IP からのアクセス頻度を制限したりします。ログ記録と分析
リクエストの基本情報(パス、参照元、UA)を記録し、分析サービスへ送信します。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 はいくつかの応答方法を提供します:
通過(そのまま続行)
return NextResponse.next();リダイレクト(新しい URL へ移動)
return NextResponse.redirect(new URL('/login', request.url));ユーザーのアドレスバーが変わります。
書き換え(内部リダイレクト、URL は不変)
return NextResponse.rewrite(new URL('/dashboard/v2', request.url));ユーザーは
/dashboardにアクセスしているつもりですが、実際には/dashboard/v2の内容が表示されます。A/B テストやバージョン切り替えに便利です。レスポンスを直接返す
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 API | Web Crypto API で代用が必要 |
| データベース | MongoDB, MySQL ネイティブドライバ | 多くの従来のDBドライバが使用不可 |
| その他 | process.emit, setImmediate | 一部の低レベル Node.js API |
実際のところ、何が困る?
- DBでユーザー認証ができない(直接クエリできない)
- 設定ファイルを読めない(
config.json等をfsで読めない) - 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;
}パフォーマンス最適化とベストプラクティス
- matcher を正確に設定する:不要なパス(特に静的ファイル)で Middleware を走らせないことが、パフォーマンス向上の第一歩です。
- ロジックを軽量に保つ:Edge での処理時間は短いほど良いです。重い処理は API ルートに任せましょう。
- 条件分岐を早めにする:不要な処理は最初の一行で弾くようにします。
- ヘッダーを活用する: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
Step1: ファイルの作成
プロジェクトルートに `middleware.ts` を作成します。 - 2
Step2: ハンドラー関数の定義
`middleware` 関数をエクスポートし、`NextRequest` を受け取ります。 - 3
Step3: ロジックの実装
リクエストを検査し、`NextResponse.next()`, `redirect()`, `rewrite()` のいずれかを返します。 - 4
Step4: Matcher の設定
`config` オブジェクトをエクスポートし、Middleware を適用するパスを制限します。
FAQ
Middleware でデータベースに接続できますか?
複数の Middleware ファイルを作成できますか?
matcher で正規表現は使えますか?
6 min read · 公開日: 2025年12月25日 · 更新日: 2026年1月22日
関連記事
Next.js ファイルアップロード完全ガイド:S3/Qiniu Cloud 署名付き URL 直接アップロード実践

Next.js ファイルアップロード完全ガイド:S3/Qiniu Cloud 署名付き URL 直接アップロード実践
Next.js Eコマース実践:カートと Stripe 決済の完全実装ガイド

Next.js Eコマース実践:カートと Stripe 決済の完全実装ガイド
Next.js ユニットテスト実践:Jest + React Testing Library 完全設定ガイド


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