Open12

Middleware

あおけんあおけん

ミドルウェアを使えば、リクエストが完了する前にコードを実行することができる。
そして、送られてきたリクエストに基づいて、

  • レスポンスを書き換え
  • リダイレクト
  • リクエストやレスポンスのヘッダーを変更
  • 直接レスポンス

など、レスポンスを変更することができる。

ミドルウェアは、キャッシュされたコンテンツとルートがマッチングされる前に実行される。
詳細はmatching-pathsを参照。

あおけんあおけん

Use Cases

ミドルウェアをアプリケーションに統合することで、
パフォーマンス、セキュリティ、ユーザーエクスペリエンスを大幅に向上させることができる。
ミドルウェアが特に効果的な一般的なシナリオには、次のようなものがある。

認証と認可

特定のページやAPIルートへのアクセスを許可する前に、
ユーザーの身元を確認し、セッションクッキーをチェックする。

サーバーサイドリダイレクト

特定の条件(ロケール、ユーザーの役割など)に基づいて、
サーバーレベルでユーザーをリダイレクトする。

パスの書き換え

リクエストプロパティに基づいて、APIルートやページへのパスを動的に書き換えることで、
A/Bテスト、機能ロールアウト、レガシーパスをサポートします。

Botの検知

ボットトラフィックを検出してブロックすることで、リソースを保護します。

ロギングと分析

ページやAPIで処理する前に、リクエストデータを取得して分析し、洞察を得る。

機能フラッグ

シームレスな機能ロールアウトやテストのために、動的に機能を有効または無効にする。


ミドルウェアが最適なアプローチではないかもしれない状況を認識することも同様に重要。
以下に、注意すべきシナリオをいくつか挙げる。

複雑なデータのフェッチと操作

ミドルウェアは、直接データを取得したり操作したりするようには設計されていない。
このような操作は、代わりにRoute Handlerまたはサーバー側のユーティリティで行う。

重い計算タスク

ミドルウェアは軽量で応答が速いものでなければならない。
そうでないとページロードの遅延を引き起こす可能性がある。
重い計算タスクや長時間実行される処理は、専用のRoute Handlerの中で行うべき。

広範なセッション管理

ミドルウェアは基本的なセッションタスクを管理できるが、
広範なセッション管理は専用の認証サービスまたは専用のRoute Handler内で管理する必要がある。

データベースの直接操作

ミドルウェア内でデータベースを直接操作することは推奨されない。
データベースとのやり取りは、Route Handleやサーバサイドのユーティリティの中で行う。

あおけんあおけん

Convention

ミドルウェアを定義するには、プロジェクトのルートにあるmiddleware.ts(または.js)ファイルを使用する。
例えば、pagesやappと同じレベルか、該当する場合はsrcの中。

あおけんあおけん

Example

// middleware.ts
import { NextResponse } from 'next/server'
import type { NextRequest } from 'next/server'
 
// This function can be marked `async` if using `await` inside
export function middleware(request: NextRequest) {
  return NextResponse.redirect(new URL('/home', request.url))
}
 
// See "Matching Paths" below to learn more
export const config = {
  matcher: '/about/:path*',
}
あおけんあおけん

Matching Paths

ミドルウェアは、プロジェクト内のすべてのルートに対して呼び出される。
マッチャーを使って、特定のルートを正確にターゲットにしたり、除外したりすることが重要。
実行順序は以下

  1. next.config.jsのheaders
  2. next.config.jsのredirects
  3. ミドルウェア(rewrites, redirectsなど)
  4. next.config.jsのbeforeFiles(rewrites)
  5. ファイルシステムのルート(public/、_next/static/、pages/、app/など)
  6. next.config.jsのafterFiles(rewrites)
  7. ダイナミック・ルート (/blog/[slug])
  8. next.config.jsからのfallback(rewrites)

ミドルウェアを実行するパスを定義するには、2つの方法がある。

Matcher

matcher を使用すると、特定のパスで実行されるミドルウェアをフィルタリングできる。

// middleware.js
export const config = {
  matcher: '/about/:path*',
}

配列構文で、単一のパスまたは複数のパスにマッチさせることができる。

// middleware.js
export const config = {
  matcher: ['/about/:path*', '/dashboard/:path*'],
}

matcher configでは完全な正規表現が使用できるので、
negative lookaheads(否定先読み)や文字マッチングなどのマッチングがサポートされている。
特定のパス以外をマッチさせるnegative lookaheadsの例。

// middleware.js
export const config = {
  matcher: [
    /*
     * Match all request paths except for the ones starting with:
     * - api (API routes)
     * - _next/static (static files)
     * - _next/image (image optimization files)
     * - favicon.ico (favicon file)
     */
    '/((?!api|_next/static|_next/image|favicon.ico).*)',
  ],
}

missing配列、has配列、またはその両方を組み合わせて使用することで、 特定のリクエストに対してミドルウェアをバイパス(スキップ)することもできる。

// middleware.js
export const config = {
  matcher: [
    /*
     * Match all request paths except for the ones starting with:
     * - api (API routes)
     * - _next/static (static files)
     * - _next/image (image optimization files)
     * - favicon.ico (favicon file)
     */
    {
      source: '/((?!api|_next/static|_next/image|favicon.ico).*)',
      missing: [
        { type: 'header', key: 'next-router-prefetch' },
        { type: 'header', key: 'purpose', value: 'prefetch' },
      ],
    },
 
    {
      source: '/((?!api|_next/static|_next/image|favicon.ico).*)',
      has: [
        { type: 'header', key: 'next-router-prefetch' },
        { type: 'header', key: 'purpose', value: 'prefetch' },
      ],
    },
 
    {
      source: '/((?!api|_next/static|_next/image|favicon.ico).*)',
      has: [{ type: 'header', key: 'x-present' }],
      missing: [{ type: 'header', key: 'x-missing', value: 'prefetch' }],
    },
  ],
}

設定されたマッチャー:

その1

/で始まらなければならない

その2

名前付きパラメータを含むことができる
/about/:pathは/about/aと/about/bにマッチするが、/about/a/cにはマッチしない

その3

名前付きパラメータ(:で始まる)に修飾子をつけることができる
/about/:path*は/about/a/b/cにマッチする。

  • はゼロ以上。?はゼロか1。+は1つ以上。
その4

括弧で囲まれた正規表現が使える。
/about/(.) は /about/:pathと同じ。

詳細はpath-to-regexpのドキュメントを参照。
後方互換性のため、Next.jsは常に/publicを/public/indexとみなす。
したがって、/public/:pathというマッチャーがマッチする。

Conditional Statements

// middleware.ts
import { NextResponse } from 'next/server'
import type { NextRequest } from 'next/server'
 
export function middleware(request: NextRequest) {
  if (request.nextUrl.pathname.startsWith('/about')) {
    return NextResponse.rewrite(new URL('/about-2', request.url))
  }
 
  if (request.nextUrl.pathname.startsWith('/dashboard')) {
    return NextResponse.rewrite(new URL('/dashboard/user', request.url))
  }
}
あおけんあおけん

NextResponse

NextResponse APIを使用すると、次のことが可能になる。

  • redirect: 別のURLへのリクエスト
  • rewrite: 指定されたURLを表示してレスポンスを返す
  • API Routes、getServerSideProps、rewriteの宛先のリクエストヘッダを設定する
  • レスポンス・クッキーを設定する
  • レスポンス・ヘッダを設定する

ミドルウェアからのレスポンスを生成するには、次のようにする。

  1. レスポンスを生成するルート (Page または Edge API Route) に書き換える。
  2. NextResponseを直接returnする
あおけんあおけん

Using Cookies

クッキーは通常のヘッダー。
リクエストでは、Cookieヘッダーに格納されます。
レスポンスでは、Set-Cookieヘッダーに格納されます。
Next.jsは、NextRequestとNextResponseのCookiesエクステンションを通して、
これらのCookiesにアクセスし、操作する便利な方法を提供します。

その1

受信リクエストに対して、クッキーは以下のメソッドを提供する。

  • get
  • getAll
  • set
  • delete
  • has
  • clear

その2

発信レスポンスに対して、クッキーは以下のメソッドを提供する

  • get
  • getAll
  • set
  • delete
// middleware.ts
import { NextResponse } from 'next/server'
import type { NextRequest } from 'next/server'
 
export function middleware(request: NextRequest) {
  // Assume a "Cookie:nextjs=fast" header to be present on the incoming request
  // Getting cookies from the request using the `RequestCookies` API
  let cookie = request.cookies.get('nextjs')
  console.log(cookie) // => { name: 'nextjs', value: 'fast', Path: '/' }
  const allCookies = request.cookies.getAll()
  console.log(allCookies) // => [{ name: 'nextjs', value: 'fast' }]
 
  request.cookies.has('nextjs') // => true
  request.cookies.delete('nextjs')
  request.cookies.has('nextjs') // => false
 
  // Setting cookies on the response using the `ResponseCookies` API
  const response = NextResponse.next()
  response.cookies.set('vercel', 'fast')
  response.cookies.set({
    name: 'vercel',
    value: 'fast',
    path: '/',
  })
  cookie = response.cookies.get('vercel')
  console.log(cookie) // => { name: 'vercel', value: 'fast', Path: '/' }
  // The outgoing response will have a `Set-Cookie:vercel=fast;path=/` header.
 
  return response
}
あおけんあおけん

Setting Headers

リクエストヘッダとレスポンスヘッダは、NextResponse APIで設定できる。

// middleware.ts
import { NextResponse } from 'next/server'
import type { NextRequest } from 'next/server'
 
export function middleware(request: NextRequest) {
  // Clone the request headers and set a new header `x-hello-from-middleware1`
  const requestHeaders = new Headers(request.headers)
  requestHeaders.set('x-hello-from-middleware1', 'hello')
 
  // You can also set request headers in NextResponse.rewrite
  const response = NextResponse.next({
    request: {
      // New request headers
      headers: requestHeaders,
    },
  })
 
  // Set a new response header `x-hello-from-middleware2`
  response.headers.set('x-hello-from-middleware2', 'hello')
  return response
}

バックエンドのウェブサーバーの設定によっては、
431 Request Header Fields Too Large エラーが発生する可能性があるため、
大きなヘッダーの設定は避けてください。

CORS

ミドルウェアでCORSヘッダーを設定し、
シンプルリクエストやプリフライトリクエストを含む、
クロスオリジンリクエストを許可することができます。

import { NextRequest, NextResponse } from 'next/server'
 
const allowedOrigins = ['https://acme.com', 'https://my-app.org']
 
const corsOptions = {
  'Access-Control-Allow-Methods': 'GET, POST, PUT, DELETE, OPTIONS',
  'Access-Control-Allow-Headers': 'Content-Type, Authorization',
}
 
export function middleware(request: NextRequest) {
  // Check the origin from the request
  const origin = request.headers.get('origin') ?? ''
  const isAllowedOrigin = allowedOrigins.includes(origin)
 
  // Handle preflighted requests
  const isPreflight = request.method === 'OPTIONS'
 
  if (isPreflight) {
    const preflightHeaders = {
      ...(isAllowedOrigin && { 'Access-Control-Allow-Origin': origin }),
      ...corsOptions,
    }
    return NextResponse.json({}, { headers: preflightHeaders })
  }
 
  // Handle simple requests
  const response = NextResponse.next()
 
  if (isAllowedOrigin) {
    response.headers.set('Access-Control-Allow-Origin', origin)
  }
 
  Object.entries(corsOptions).forEach(([key, value]) => {
    response.headers.set(key, value)
  })
 
  return response
}
 
export const config = {
  matcher: '/api/:path*',
}
あおけんあおけん

Producing a Response

ResponseやNextResponseのインスタンスを返すことで、
ミドルウェアから直接応答することができる。

// middleware.ts
import { NextRequest } from 'next/server'
import { isAuthenticated } from '@lib/auth'
 
// Limit the middleware to paths starting with `/api/`
export const config = {
  matcher: '/api/:function*',
}
 
export function middleware(request: NextRequest) {
  // Call our authentication function to check the request
  if (!isAuthenticated(request)) {
    // Respond with JSON indicating an error message
    return Response.json(
      { success: false, message: 'authentication failed' },
      { status: 401 }
    )
  }
}

waitUntil and NextFetchEvent

NextFetchEventオブジェクトは、
ネイティブのFetchEventオブジェクトを拡張し、
waitUntil()メソッドを含んでいる。

waitUntil()メソッドは、
引数にPromiseを取り、Promiseが落ち着くまでミドルウェアのライフタイムを延長する。
これは、バックグラウンドで作業を行う場合に便利です。

// middleware.ts
import { NextResponse } from 'next/server'
import type { NextFetchEvent, NextRequest } from 'next/server'
 
export function middleware(req: NextRequest, event: NextFetchEvent) {
  event.waitUntil(
    fetch('https://my-analytics-platform.com', {
      method: 'POST',
      body: JSON.stringify({ pathname: req.nextUrl.pathname }),
    })
  )
 
  return NextResponse.next()
}
あおけんあおけん

Advanced Middleware Flags

Next.jsのv13.1では、高度なユースケースに対応するために、ミドルウェア用に2つの追加フラグ、skipMiddlewareUrlNormalizeとskipTrailingSlashRedirectが導入された。

skipTrailingSlashRedirectは、Next.jsのリダイレクトによる末尾スラッシュの追加や削除を無効にします。
これにより、ミドルウェア内部のカスタム処理で、
あるパスでは末尾のスラッシュを維持し、
他のパスでは維持しないようにすることができます。

// next.config.js
module.exports = {
  skipTrailingSlashRedirect: true,
}
// middleware.js
const legacyPrefixes = ['/docs', '/blog']
 
export default async function middleware(req) {
  const { pathname } = req.nextUrl
 
  if (legacyPrefixes.some((prefix) => pathname.startsWith(prefix))) {
    return NextResponse.next()
  }
 
  // apply trailing slash handling
  if (
    !pathname.endsWith('/') &&
    !pathname.match(/((?!\.well-known(?:\/.*)?)(?:[^/]+\/)*[^/]+\.\w+)/)
  ) {
    req.nextUrl.pathname += '/'
    return NextResponse.redirect(req.nextUrl)
  }
}

skipMiddlewareUrlNormalizeを使用すると、
Next.jsのURL正規化を無効にして、直接訪問とクライアント遷移の処理を同じにする。
いくつかの高度なケースでは、このオプションは、元のURLを使用することにより、完全な制御を提供する。

// next.config.js
module.exports = {
  skipMiddlewareUrlNormalize: true,
}
// middleware.js
export default async function middleware(req) {
  const { pathname } = req.nextUrl
 
  // GET /_next/data/build-id/hello.json
 
  console.log(pathname)
  // with the flag this now /_next/data/build-id/hello.json
  // without the flag this would be normalized to /hello
}
あおけんあおけん

Runtime

ミドルウェアは現在、Edgeランタイムのみをサポートしている。
Node.jsランタイムは使用できない。

あおけんあおけん

Version History

Version Changes
v13.1.0 高度なミドルウェアのフラグが追加
v13.0.0 ミドルウェアはリクエスト・ヘッダ、レスポンス・ヘッダを修正し、レスポンスを送信することができる。
v12.2.0 ミドルウェアは安定しています。
v12.0.9 Edgeランタイムで絶対URLを強制する
v12.0.0 ミドルウェア(ベータ版)追加