Next.js Middleware実践ガイド

中級 | 25分 で読める | 2025.01.10

公式ドキュメント

Middlewareとは

Next.js Middlewareは、リクエストが完了する前にコードを実行できる機能です。Edge Runtimeで動作し、高速なレスポンスを実現します。

注意: middleware.tsはプロジェクトルートに配置してください。src/ディレクトリ内に置く場合はsrc/middleware.tsです。

セットアップ

// middleware.ts(プロジェクトルート)
import { NextResponse } from 'next/server';
import type { NextRequest } from 'next/server';

export function middleware(request: NextRequest) {
 console.log('Request:', request.nextUrl.pathname);
 return NextResponse.next();
}

// 適用するパスを指定
export const config = {
 matcher: ['/dashboard/:path*', '/api/:path*'],
};

認証ミドルウェア

実践メモ: matcherで対象パスを絞り込むと、静的ファイルや公開ページでミドルウェアが実行されるのを防げます。

// middleware.ts
import { NextResponse } from 'next/server';
import type { NextRequest } from 'next/server';
import { verifyToken } from './lib/auth';

const publicPaths = ['/', '/login', '/signup', '/api/auth'];

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

 // 公開パスはスキップ
 if (publicPaths.some(path => pathname.startsWith(path))) {
 return NextResponse.next();
 }

 // 静的ファイルはスキップ
 if (pathname.match(/\.(ico|png|jpg|svg|css|js)$/)) {
 return NextResponse.next();
 }

 // トークン検証
 const token = request.cookies.get('auth-token')?.value;

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

 try {
 const user = await verifyToken(token);

 // ユーザー情報をヘッダーに追加
 const response = NextResponse.next();
 response.headers.set('x-user-id', user.id);
 response.headers.set('x-user-role', user.role);

 return response;
 } catch (error) {
 // トークン無効 → ログインへリダイレクト
 const response = NextResponse.redirect(new URL('/login', request.url));
 response.cookies.delete('auth-token');
 return response;
 }
}

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

ロールベースアクセス制御

// middleware.ts
const roleRoutes: Record<string, string[]> = {
 admin: ['/admin', '/dashboard', '/settings'],
 editor: ['/dashboard', '/content'],
 user: ['/dashboard'],
};

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

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

 const user = await verifyToken(token);
 const allowedPaths = roleRoutes[user.role] || [];

 const hasAccess = allowedPaths.some(path => pathname.startsWith(path));

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

 return NextResponse.next();
}

リダイレクト処理

// middleware.ts
const redirects: Record<string, string> = {
 '/old-page': '/new-page',
 '/blog/old-post': '/articles/new-post',
};

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

 // リダイレクト設定のチェック
 if (redirects[pathname]) {
 return NextResponse.redirect(
 new URL(redirects[pathname], request.url),
 301 // 永続的リダイレクト
 );
 }

 // トレイリングスラッシュの統一
 if (pathname !== '/' && pathname.endsWith('/')) {
 return NextResponse.redirect(
 new URL(pathname.slice(0, -1), request.url),
 308
 );
 }

 return NextResponse.next();
}

レート制限

ポイント: @upstash/ratelimitを使うとEdge Runtimeで動作するレート制限を簡単に実装できます。

// middleware.ts
import { Ratelimit } from '@upstash/ratelimit';
import { Redis } from '@upstash/redis';

const ratelimit = new Ratelimit({
 redis: Redis.fromEnv(),
 limiter: Ratelimit.slidingWindow(10, '10 s'), // 10秒で10リクエスト
 analytics: true,
});

export async function middleware(request: NextRequest) {
 // APIルートにのみ適用
 if (!request.nextUrl.pathname.startsWith('/api')) {
 return NextResponse.next();
 }

 const ip = request.ip ?? '127.0.0.1';
 const { success, limit, reset, remaining } = await ratelimit.limit(ip);

 if (!success) {
 return new NextResponse(
 JSON.stringify({ error: 'Too many requests' }),
 {
 status: 429,
 headers: {
 'Content-Type': 'application/json',
 'X-RateLimit-Limit': limit.toString(),
 'X-RateLimit-Remaining': remaining.toString(),
 'X-RateLimit-Reset': reset.toString(),
 },
 }
 );
 }

 const response = NextResponse.next();
 response.headers.set('X-RateLimit-Limit', limit.toString());
 response.headers.set('X-RateLimit-Remaining', remaining.toString());

 return response;
}

多言語対応(i18n)

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

const locales = ['ja', 'en', 'ko'];
const defaultLocale = 'ja';

function getLocale(request: NextRequest): string {
 const headers = { 'accept-language': request.headers.get('accept-language') || '' };
 const languages = new Negotiator({ headers }).languages();

 try {
 return match(languages, locales, defaultLocale);
 } catch {
 return defaultLocale;
 }
}

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

 // ロケールがパスに含まれているかチェック
 const pathnameHasLocale = locales.some(
 locale => pathname.startsWith(`/${locale}/`) || pathname === `/${locale}`
 );

 if (pathnameHasLocale) return NextResponse.next();

 // ロケールをパスに追加
 const locale = getLocale(request);
 request.nextUrl.pathname = `/${locale}${pathname}`;

 return NextResponse.redirect(request.nextUrl);
}

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

A/Bテスト

// middleware.ts
const EXPERIMENT_COOKIE = 'ab-experiment';

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

 // 既存のバリアントをチェック
 let variant = request.cookies.get(EXPERIMENT_COOKIE)?.value;

 if (!variant) {
 // 新規ユーザーにランダム割り当て
 variant = Math.random() < 0.5 ? 'control' : 'variant';
 response.cookies.set(EXPERIMENT_COOKIE, variant, {
 maxAge: 60 * 60 * 24 * 30, // 30日
 });
 }

 // ヘッダーで渡す
 response.headers.set('x-experiment-variant', variant);

 return response;
}

関連記事

Middlewareでやりすぎない

Next.js Middlewareはリクエストの入口で処理できる便利な仕組みですが、何でも入れる場所ではありません。認証チェック、リダイレクト、A/Bテスト、言語判定など、軽く早く終わる処理に向いています。

重いDBアクセスや複雑なビジネスロジックを入れると、レスポンス全体の遅延やEdge Runtimeの制約に引っかかりやすくなります。Middlewareで判断することと、ページやAPI側で処理することを分けて設計します。

実装時の確認

  • matcherで対象パスを絞っているか
  • 静的ファイルやAPIを誤って処理していないか
  • Edge Runtimeで使えないNode.js APIを呼んでいないか
  • リダイレクトループが起きないか

まとめ

Next.js Middlewareは、認証、リダイレクト、レート制限、i18nなど、リクエスト前の処理に最適です。Edge Runtimeの制約を理解した上で活用しましょう。

参考リソース

← 一覧に戻る
PR
PR
PR
PR