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 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 });処理を中断し、ユーザーに直接コンテンツを返します。
もう少し実用的な例:カスタム 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.ts が proxy.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 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 {
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.jscryptoに依存)- 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:
jose(jsonwebtokenは不可) - 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*']
}
ポイント:
fromでログイン後の戻り先を保持- 有効期限が近い token は更新フラグを header に載せる
- ユーザー情報を 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
スタックトレースで原因ライブラリを特定。よくあるのは fs、jsonwebtoken、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 サーバーを再起動。
デバッグまとめ
x-middleware-executedなどの header で通過確認- 処理を段階的にコメントアウトして切り分け
- Vercel の Edge Function Logs
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: Middleware ファイルを作成する
作成手順:
• 配置:middleware.ts(プロジェクトルート)
• config オブジェクトで matcher をエクスポート
• middleware 関数でリクエストを処理
基本構造:
export const config = {
matcher: '/dashboard/:path*'
}
export function middleware(request: NextRequest) {
// 処理ロジック
} - 2
ステップ2: matcher ルールを設定する
マッチ規則:
• 単一パス:'/dashboard'
• 動的ルート:'/dashboard/:path*'(* で多階層にマッチ)
• 複数パス:['/dashboard/:path*', '/admin/:path*']
• 除外:否定先読み '/((?!api|_next/static|_next/image|favicon.ico).*)'
注意:
• 動的ルートは * 必須
• 静的リソース(_next/static 等)は除外
• ログインページなど公開ページは傍受しない - 3
ステップ3: ルート保護(認証)を実装する
手順:
1. cookie から token を読む
2. token を検証(JWT または API 呼び出し)
3. 未ログインはログインページへリダイレクト
4. ログイン済みはそのまま通過
要点:
• NextRequest.cookies で cookie 取得
• NextResponse.redirect でリダイレクト
• NextResponse.next で続行
• 無限リダイレクトループに注意 - 4
ステップ4: 国際化(言語切替)を実装する
手順:
1. 言語設定を検出(cookie、header、デフォルト)
2. パスからリダイレクト要否を判断
3. URL に言語プレフィックスを付与
4. 言語 cookie を設定
要点:
• request.headers.get('accept-language')
• request.nextUrl.pathname でパス取得
• NextResponse.rewrite で URL 書き換え
• URL 構造を変えずに言語切替も可能 - 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: テストとデバッグ
テスト項目:
• マッチ対象の全パス
• 非マッチパス(傍受されないこと)
• リダイレクトロジック
• Edge Runtime 互換性
デバッグ方法:
• middleware に console.log
• ブラウザの Network タブ
• Vercel Edge Functions ログ
• Next.js 開発モードの警告
FAQ
Middleware の matcher 設定が効かないときは?
1) matcher パスが正しいか(動的ルートには * が必要)
2) 静的リソースを除外しているか
3) パス形式が Next.js 対応か(任意の正規表現ではなく公式形式)
middleware に console.log を入れ、どのパスがマッチしたか確認してください。
多階層パスがマッチしないのはなぜ?
Edge Runtime でライブラリが使えないときは?
対策:
1) パッケージの Edge 対応を確認
2) Web 標準 API で代替
3) 複雑な処理は API ルートまたは Server Component へ
4) Edge 対応の代替ライブラリを使う
無限リダイレクトループを防ぐには?
Middleware でデータベースに接続できますか?
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 ルートの違いは?
• Edge Runtime で実行
• ページや API 到達前に実行
• 軽量な傍受・転送向き
API ルート:
• Node.js ランタイム
• 完全な Node.js API と DB にアクセス可能
• 複雑なビジネスロジック向き
7分で読めます · 公開日: 2025年12月25日 · 更新日: 2026年6月8日
Next.js 完全ガイド
検索からこのページに来た場合は、前後の記事もあわせて読むと同じテーマの理解がかなり早く深まります。
前の記事
Next.js 15 実践:週末で本番級ブログシステムを構築した方法
Next.js 15 + Server Actions + Prisma の実践事例で、週末に本番級フルスタックブログを構築する手順を解説。完全なコード、つまずきポイント、パフォーマンス最適化まで網羅。
第 2 / 47 記事
次の記事
Next.js を Vercel にデプロイする完全ガイド:環境変数、ドメイン設定、パフォーマンス監視
Next.js を Vercel にデプロイする完全ガイド。環境変数の設定、カスタムドメインの紐付け、SSL 証明書、パフォーマンス監視までを網羅し、初心者がよく踏む罠を避けます。
第 4 / 47 記事
関連記事
Next.js App Router 入門ガイド:コア概念と基本操作を解説
Next.js App Router 入門ガイド:コア概念と基本操作を解説
Next.js データベース選定ガイド:PostgreSQL、MySQL、MongoDB とクラウドサービスの完全比較
Next.js データベース選定ガイド:PostgreSQL、MySQL、MongoDB とクラウドサービスの完全比較
Next.js 高度なルーティング実践:Route Groups・ネストレイアウト・Parallel Routes・Intercepting Routes 完全ガイド
コメント
GitHubアカウントでログインしてコメントできます