Middlewareとは
Next.js Middlewareは、リクエストが完了する前にコードを実行できる機能です。Edge Runtimeで動作し、高速なレスポンスを実現します。
セットアップ
// 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*'],
};
認証ミドルウェア
// 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();
}
レート制限
// 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;
}
関連記事
- Next.js App Router - ルーティング基礎
- 認証実装 - OAuth認証
- JWTの仕組み - トークン認証理論
まとめ
Next.js Middlewareは、認証、リダイレクト、レート制限、i18nなど、リクエスト前の処理に最適です。Edge Runtimeの制約を理解した上で活用しましょう。
← Back to list