Next.js Middleware実践ガイド

中級 | 25分 read | 2025.01.10

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 Middlewareは、認証、リダイレクト、レート制限、i18nなど、リクエスト前の処理に最適です。Edge Runtimeの制約を理解した上で活用しましょう。

← Back to list