日本語

Next.jsミドルウェアを使用した高度なリクエスト変更テクニックについて説明します。堅牢なWebアプリケーションのために、複雑なルーティング、認証、A/Bテスト、ローカリゼーション戦略を処理する方法を学びます。

Next.jsミドルウェアのエッジケース:リクエスト変更パターンをマスターする

Next.jsミドルウェアは、アプリケーションのルートに到達する前にリクエストをインターセプトして変更するための強力なメカニズムを提供します。この機能により、簡単な認証チェックから、複雑なA/Bテストシナリオや国際化戦略まで、幅広い可能性が開かれます。ただし、ミドルウェアを効果的に活用するには、そのエッジケースと潜在的な落とし穴を深く理解する必要があります。この包括的なガイドでは、高度なリクエスト変更パターンを検討し、堅牢でパフォーマンスの高いNext.jsアプリケーションを構築するのに役立つ実践的な例と実用的な洞察を提供します。

Next.jsミドルウェアの基礎を理解する

高度なパターンに入る前に、Next.jsミドルウェアの基本を復習しましょう。ミドルウェア関数は、リクエストが完了する前に実行され、次のことが可能になります。

ミドルウェア関数は、middleware.jsまたはmiddleware.tsファイル内の/pagesまたは/appディレクトリに存在します(Next.jsのバージョンとセットアップによって異なります)。それらは、受信リクエストを表すNextRequestオブジェクトを受け取り、後続の動作を制御するためにNextResponseオブジェクトを返すことができます。

例:基本的な認証ミドルウェア

この例は、簡単な認証チェックを示しています。ユーザーが認証されていない場合(たとえば、Cookieに有効なトークンがない場合)、ログインページにリダイレクトされます。


import { NextResponse } from 'next/server'
import type { NextRequest } from 'next/server'

export function middleware(request: NextRequest) {
  const authToken = request.cookies.get('authToken')

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

  return NextResponse.next()
}

export const config = {
  matcher: ['/protected/:path*'],
}

このミドルウェアは、/protected/:path*に一致するルートに対してのみ実行されます。authTokenCookieの存在を確認します。 Cookieが見つからない場合、ユーザーは/loginページにリダイレクトされます。それ以外の場合、NextResponse.next()を使用してリクエストは通常どおり続行できます。

高度なリクエスト変更パターン

次に、Next.jsミドルウェアの真のパワーを示す、高度なリクエスト変更パターンをいくつか見てみましょう。

1. Cookieを使用したA/Bテスト

A/Bテストは、ユーザーエクスペリエンスを最適化するための重要な手法です。ミドルウェアを使用して、ユーザーをアプリケーションのさまざまなバリエーションにランダムに割り当て、その動作を追跡できます。このパターンは、Cookieを使用してユーザーの割り当てられたバリアントを保持します。

例:ランディングページのA/Bテスト


import { NextResponse } from 'next/server'
import type { NextRequest } from 'next/server'

const VARIANT_A = 'variantA'
const VARIANT_B = 'variantB'

export function middleware(request: NextRequest) {
  let variant = request.cookies.get('variant')?.value

  if (!variant) {
    // Randomly assign a variant
    variant = Math.random() < 0.5 ? VARIANT_A : VARIANT_B
    const response = NextResponse.next()
    response.cookies.set('variant', variant)
    return response
  }

  if (variant === VARIANT_A) {
    return NextResponse.rewrite(new URL('/variant-a', request.url))
  } else if (variant === VARIANT_B) {
    return NextResponse.rewrite(new URL('/variant-b', request.url))
  }

  return NextResponse.next()
}

export const config = {
  matcher: ['/'],
}

この例では、ユーザーがルートパス(/)に初めてアクセスすると、ミドルウェアはそれらをvariantAまたはvariantBのいずれかにランダムに割り当てます。このバリアントはCookieに保存されます。同じユーザーからの後続のリクエストは、割り当てられたバリアントに応じて、/variant-aまたは/variant-bに書き換えられます。これにより、異なるランディングページを提供し、どちらのパフォーマンスが優れているかを追跡できます。 Next.jsアプリケーションで/variant-aおよび/variant-bのルートが定義されていることを確認してください。

グローバルな考慮事項: A/Bテストを実行する場合は、地域ごとのバリエーションを考慮してください。北米で共感を呼ぶデザインは、アジアでは効果がない可能性があります。(IPアドレスの検索またはユーザー設定を通じて取得した)ジオロケーションデータを使用して、A/Bテストを特定の地域に合わせることができます。

2. URL書き換えによるローカリゼーション(i18n)

国際化(i18n)は、グローバルなオーディエンスにリーチするために不可欠です。ミドルウェアを使用して、ユーザーの優先言語を自動的に検出し、サイトの適切なローカライズされたバージョンにリダイレクトできます。

例:`Accept-Language`ヘッダーに基づいてリダイレクトする


import { NextResponse } from 'next/server'
import type { NextRequest } from 'next/server'

const SUPPORTED_LANGUAGES = ['en', 'fr', 'es', 'de']
const DEFAULT_LANGUAGE = 'en'

function getPreferredLanguage(request: NextRequest): string {
  const acceptLanguage = request.headers.get('accept-language')
  if (!acceptLanguage) {
    return DEFAULT_LANGUAGE
  }

  const languages = acceptLanguage.split(',').map((lang) => lang.split(';')[0].trim())

  for (const lang of languages) {
    if (SUPPORTED_LANGUAGES.includes(lang)) {
      return lang
    }
  }

  return DEFAULT_LANGUAGE
}

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

  // Check if there's an existing locale in the pathname
  if (
    SUPPORTED_LANGUAGES.some(
      (locale) => pathname.startsWith(`/${locale}/`) || pathname === `/${locale}`
    )
  ) {
    return NextResponse.next()
  }

  const preferredLanguage = getPreferredLanguage(request)

  return NextResponse.redirect(
    new URL(`/${preferredLanguage}${pathname}`, request.url)
  )
}

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

このミドルウェアは、リクエストからAccept-Languageヘッダーを抽出し、ユーザーの優先言語を決定します。 URLに言語プレフィックス(例:/en/about)がまだ含まれていない場合、ミドルウェアはユーザーを適切なローカライズされたURL(例:フランス語の場合は/fr/about)にリダイレクトします。 `/pages`または`/app`ディレクトリに、異なるロケールに対応する適切なフォルダー構造があることを確認してください。たとえば、`/pages/en/about.js`ファイルと`/pages/fr/about.js`ファイルが必要になります。

グローバルな考慮事項: i18nの実装で、右から左に記述する言語(例:アラビア語、ヘブライ語)が正しく処理されることを確認してください。また、コンテンツ配信ネットワーク(CDN)を使用して、ローカライズされたアセットをユーザーに近いサーバーから提供し、パフォーマンスを向上させることも検討してください。

3. 機能フラグ

機能フラグを使用すると、新しいコードをデプロイせずに、アプリケーションの機能を有効または無効にできます。これは、新しい機能を段階的に展開したり、本番環境で機能をテストしたりする場合に特に便利です。ミドルウェアを使用して、機能フラグのステータスを確認し、それに応じてリクエストを変更できます。

例:ベータ機能を有効にする


import { NextResponse } from 'next/server'
import type { NextRequest } from 'next/server'

const BETA_FEATURE_ENABLED = process.env.BETA_FEATURE_ENABLED === 'true'

export function middleware(request: NextRequest) {
  if (BETA_FEATURE_ENABLED && request.nextUrl.pathname.startsWith('/new-feature')) {
    return NextResponse.next()
  }

  // Optionally redirect to a "feature unavailable" page
  return NextResponse.rewrite(new URL('/feature-unavailable', request.url))
}

export const config = {
  matcher: ['/new-feature/:path*'],
}

このミドルウェアは、BETA_FEATURE_ENABLED環境変数の値を確認します。 trueに設定されていて、ユーザーが/new-featureのルートにアクセスしようとしている場合、リクエストは続行できます。それ以外の場合、ユーザーは/feature-unavailableページにリダイレクトされます。さまざまな環境(開発、ステージング、本番)に合わせて環境変数を適切に構成することを忘れないでください。

グローバルな考慮事項:機能フラグを使用する場合は、すべての地域の規制に準拠していない可能性のある機能を有効にする法的影響を考慮してください。たとえば、データプライバシーに関連する機能は、特定の国では無効にする必要がある場合があります。

4. デバイスの検出とアダプティブルーティング

最新のWebアプリケーションは、応答性が高く、さまざまな画面サイズとデバイスの機能に適応する必要があります。ミドルウェアを使用して、ユーザーのデバイスタイプを検出し、最適化されたバージョンのサイトにリダイレクトできます。

例:モバイルユーザーをモバイル向けに最適化されたサブドメインにリダイレクトする


import { NextResponse } from 'next/server'
import type { NextRequest } from 'next/server'
import { device } from 'detection'

export function middleware(request: NextRequest) {
  const userAgent = request.headers.get('user-agent')

  if (userAgent) {
    const deviceType = device(userAgent)

    if (deviceType.type === 'phone') {
      const mobileUrl = new URL(request.url)
      mobileUrl.hostname = 'm.example.com'
      return NextResponse.redirect(mobileUrl)
    }
  }

  return NextResponse.next()
}

export const config = {
  matcher: ['/'],
}

この例では、`detection`ライブラリを使用して、User-Agentヘッダーに基づいてユーザーのデバイスタイプを判別します。ユーザーが携帯電話を使用している場合、m.example.comサブドメインにリダイレクトされます(モバイル向けに最適化されたバージョンのサイトがそこにホストされていると仮定します)。 `detection`パッケージをインストールすることを忘れないでください:`npm install detection`。

グローバルな考慮事項:デバイス検出ロジックで、デバイスの使用における地域ごとのバリエーションが考慮されていることを確認してください。たとえば、一部の開発途上国では、フィーチャーフォンがまだ普及しています。より堅牢なソリューションのために、User-Agent検出とレスポンシブデザイン技術の組み合わせを使用することを検討してください。

5. リクエストヘッダーの拡張

ミドルウェアは、アプリケーションルートで処理される前に、リクエストヘッダーに情報を追加できます。これは、ユーザーロール、認証ステータス、リクエストIDなどのカスタムメタデータを追加する場合に役立ちます。これらのメタデータは、アプリケーションロジックで使用できます。

例:リクエストIDの追加


import { NextResponse } from 'next/server'
import type { NextRequest } from 'next/server'
import { v4 as uuidv4 } from 'uuid'

export function middleware(request: NextRequest) {
  const requestId = uuidv4()
  const response = NextResponse.next()
  response.headers.set('x-request-id', requestId)
  return response
}

export const config = {
  matcher: ['/api/:path*'], // Only apply to API routes
}

このミドルウェアは、uuidライブラリを使用して一意のリクエストIDを生成し、x-request-idヘッダーに追加します。このIDは、ロギング、トレース、およびデバッグの目的で使用できます。 `uuid`パッケージをインストールすることを忘れないでください:`npm install uuid`。

グローバルな考慮事項:カスタムヘッダーを追加する場合は、ヘッダーサイズの制限に注意してください。これらの制限を超えると、予期しないエラーが発生する可能性があります。また、ヘッダーに追加された機密情報が適切に保護されていることを確認してください。特に、アプリケーションがリバースプロキシまたはCDNの背後にある場合は注意が必要です。

6. セキュリティの強化:レート制限

ミドルウェアは、レート制限を実装することにより、悪意のある攻撃に対する最初の防衛線として機能できます。これにより、クライアントが特定の時間枠内に行うことができるリクエストの数を制限することにより、不正使用を防ぎます。

例:単純なストアを使用した基本的なレート制限


import { NextResponse } from 'next/server'
import type { NextRequest } from 'next/server'

const requestCounts: { [ip: string]: number } = {}
const WINDOW_SIZE_MS = 60000; // 1 minute
const MAX_REQUESTS_PER_WINDOW = 100;

export function middleware(request: NextRequest) {
  const clientIP = request.ip || '127.0.0.1' // Get client IP, default to localhost for local testing

  if (!requestCounts[clientIP]) {
    requestCounts[clientIP] = 0;
  }

  requestCounts[clientIP]++;

  if (requestCounts[clientIP] > MAX_REQUESTS_PER_WINDOW) {
    return new NextResponse(
      JSON.stringify({ message: 'Too many requests' }),
      { status: 429, headers: { 'Content-Type': 'application/json' } }
    );
  }

  // Reset count after window
  setTimeout(() => {
    requestCounts[clientIP]--;
    if (requestCounts[clientIP] <= 0) {
        delete requestCounts[clientIP];
    }
  }, WINDOW_SIZE_MS);

  return NextResponse.next();
}

export const config = {
  matcher: ['/api/:path*'], // Apply to all API routes
}

この例では、各IPアドレスからのリクエスト数を追跡するために、単純なインメモリストア(requestCounts)を保持します。クライアントがWINDOW_SIZE_MS内のMAX_REQUESTS_PER_WINDOWを超えた場合、ミドルウェアは429 Too Many Requestsエラーを返します。 重要:これは簡略化された例であり、スケーリングできず、サービス拒否攻撃に対して脆弱であるため、本番環境には適していません。本番環境で使用する場合は、Redisなどのより堅牢なレート制限ソリューション、または専用のレート制限サービスの使用を検討してください。

グローバルな考慮事項:レート制限戦略は、アプリケーションの特定の特性とユーザーの地理的な分布に合わせて調整する必要があります。地域またはユーザーセグメントごとに異なるレート制限を使用することを検討してください。

エッジケースと潜在的な落とし穴

ミドルウェアは強力なツールですが、その制限と潜在的な落とし穴に注意することが不可欠です。

Next.jsミドルウェアを使用するためのベストプラクティス

Next.jsミドルウェアのメリットを最大化し、潜在的な問題を回避するには、次のベストプラクティスに従ってください。

結論

Next.jsミドルウェアは、リクエストを変更し、エッジでアプリケーションの動作をカスタマイズするための強力な方法を提供します。このガイドで説明されている高度なリクエスト変更パターンを理解することで、堅牢でパフォーマンスが高く、グローバルに対応できるNext.jsアプリケーションを構築できます。エッジケースと潜在的な落とし穴を慎重に検討し、上記のアウトラインで示されたベストプラクティスに従って、ミドルウェア関数が信頼性が高く保守可能であることを確認してください。ミドルウェアの力を活用して、卓越したユーザーエクスペリエンスを作成し、Webアプリケーションの新しい可能性を解き放ちましょう。