Middleware アップグレードガイド
汎用利用(GA)に向けてMiddlewareの改善を進める中で、皆様からのフィードバックに基づき、Middleware API(およびアプリケーションでのMiddlewareの定義方法)にいくつかの変更を加えました。
このアップグレードガイドでは、変更点、その理由、そして既存のMiddlewareを新しいAPIに移行する方法について説明します。このガイドは、以下のようなNext.js開発者を対象としています。
- ベータ版のNext.js Middleware機能を利用している
- Next.js の次の安定版(v12.2)にアップグレードすることを希望している
最新リリース(npm i next@latest)で、今すぐMiddlewareの利用をアップグレードできます。
注意:このガイドで説明されている変更は、Next.js
12.2に含まれています。12.2(またはNext.jsのcanaryビルド)に移行するまで、ネストされたMiddlewareを含む現在のサイト構造を維持できます。
ESLint を設定している場合、Next.js のバージョンと同じバージョンが使用されていることを確認するために、npm i eslint-config-next@latest --save-dev を実行して ESLint 設定をアップグレードする必要があります。変更を有効にするために VSCode を再起動する必要がある場合もあります。
Vercel で Next.js Middleware を使用する
Vercel で Next.js を使用している場合、Middleware を使用した既存のデプロイは引き続き機能し、Middleware を使用してサイトをデプロイし続けることができます。サイトを Next.js の次の安定版(v12.2)にアップグレードする際には、Middleware を更新するためにこのアップグレードガイドに従う必要があります。
破壊的変更
- ネストされた Middleware の廃止
- レスポンスボディの廃止
- Cookies API の刷新
- 新しい User-Agent ヘルパー
- Page Match Data の廃止
- 内部 Next.js リクエストでの Middleware 実行
ネストされた Middleware の廃止
変更点の概要
- pagesフォルダの隣に単一の Middleware ファイルを定義します。
- ファイル名をアンダースコアでプレフィックスする必要はありません。
- エクスポートされた config オブジェクトを使用して、マッチするルートを定義するためにカスタムマッチャーを使用できます。
説明
以前は、任意のレベルのpagesディレクトリの下に_middleware.tsファイルを作成できました。Middlewareの実行は、作成されたファイルパスに基づいていました。
お客様からのフィードバックに基づき、このAPIを単一のルートMiddlewareに置き換えました。これにより、以下の改善がもたらされます。
- 実行速度の向上とレイテンシの削減: ネストされたMiddlewareでは、単一のリクエストが複数のMiddleware関数を呼び出す可能性がありました。単一のMiddlewareは単一の関数実行となるため、より効率的です。
- コスト削減: Middlewareの使用は、呼び出しごとに課金されます。ネストされたMiddlewareを使用すると、単一のリクエストが複数のMiddleware関数を呼び出す可能性があり、リクエストごとに複数のMiddleware料金が発生していました。単一のMiddlewareは、リクエストごとに1回の呼び出しとなり、よりコスト効率が高くなります。
- Middleware でルート以外の条件で簡単にフィルタリング可能: ネストされたMiddlewareでは、Middleware ファイルはpagesディレクトリに配置され、Middleware はリクエストパスに基づいて実行されていました。単一のルートMiddlewareに移行することで、リクエストパスに基づいてコードを実行し続けることができますが、cookiesやリクエストヘッダーの存在など、他の条件に基づいて Middleware をより便利に実行できるようになりました。
- 決定論的な実行順序: ネストされたMiddlewareでは、単一のリクエストが複数のMiddleware関数に一致する可能性がありました。例えば、/dashboard/users/*へのリクエストは、/dashboard/users/_middleware.tsと/dashboard/_middleware.jsの両方に定義された Middleware を呼び出しました。しかし、実行順序を推測するのは困難でした。単一のルートMiddlewareに移行することで、実行順序がより明確に定義されます。
- Next.js Layouts (RFC) のサポート: 単一のルートMiddlewareに移行することで、Next.js の新しい Layouts (RFC) のサポートが向上します。
アップグレード方法
アプリケーションには単一のMiddleware ファイルを宣言してください。このファイルは pages ディレクトリの隣に配置し、_ プレフィックスを付けずに名前を付けてください。Middleware ファイルは、.ts または .js の拡張子を持つことができます。
Middleware はアプリ内のすべてのルートに対して呼び出され、カスタムマッチャーを使用してマッチするフィルターを定義できます。以下は、/about/* および /dashboard/:path* にトリガーされる Middleware の例で、カスタムマッチャーはエクスポートされた config オブジェクトで定義されます。
import { NextResponse } from 'next/server'
import type { NextRequest } from 'next/server'
 
export function middleware(request: NextRequest) {
  return NextResponse.rewrite(new URL('/about-2', request.url))
}
 
// Supports both a single string value or an array of matchers
export const config = {
  matcher: ['/about/:path*', '/dashboard/:path*'],
}マッチャー config では、完全な正規表現も使用できるため、ネガティブルックアヘッドや文字マッチングなどがサポートされます。特定のパスを除くすべてにマッチさせるネガティブルックアヘッドの例を以下に示します。
export const config = {
  matcher: [
    /*
     * Match all request paths except for the ones starting with:
     * - api (API routes)
     * - _next/static (static files)
     * - favicon.ico (favicon file)
     */
    '/((?!api|_next/static|favicon.ico).*)',
  ],
}config オプションはリクエストごとに呼び出されないため推奨されますが、条件付きステートメントを使用して、特定のパスに一致する場合にのみ Middleware を実行することもできます。条件を使用する利点の 1 つは、Middleware が実行される順序を明示的に定義できることです。次の例は、以前ネストされていた 2 つの Middleware をマージする方法を示しています。
import type { NextRequest } from 'next/server'
 
export function middleware(request: NextRequest) {
  if (request.nextUrl.pathname.startsWith('/about')) {
    // This logic is only applied to /about
  }
 
  if (request.nextUrl.pathname.startsWith('/dashboard')) {
    // This logic is only applied to /dashboard
  }
}レスポンスボディの廃止
変更点の概要
- Middleware はレスポンスボディを生成できなくなりました。
- Middleware がボディで応答した場合、実行時エラーがスローされます。
- rewrite/- redirectを使用して、レスポンスを処理するページ/API に移行してください。
説明
クライアントサイドナビゲーションとサーバーサイドナビゲーションの違いを尊重し、開発者が安全でない Middleware を構築しないようにするために、Middleware でのレスポンスボディの送信機能を削除しています。これにより、Middleware は rewrite、redirect、または受信リクエストの変更(例:cookies の設定)にのみ使用されることが保証されます。
以下のパターンは機能しなくなります。
new Response('a text value')
new Response(streamOrBuffer)
new Response(JSON.stringify(obj), { headers: 'application/json' })
NextResponse.json()アップグレード方法
Middleware が応答に使用されていた場合(認証など)、rewrite/redirect を使用して、認証エラー、ログインフォーム、または API Route を表示するページに移行する必要があります。
変更前
import { NextResponse } from 'next/server'
import type { NextRequest } from 'next/server'
import { isAuthValid } from './lib/auth'
 
export function middleware(request: NextRequest) {
  // Example function to validate auth
  if (isAuthValid(request)) {
    return NextResponse.next()
  }
 
  return NextResponse.json({ message: 'Auth required' }, { status: 401 })
}変更後
import { NextResponse } from 'next/server'
import type { NextRequest } from 'next/server'
import { isAuthValid } from './lib/auth'
 
export function middleware(request: NextRequest) {
  // Example function to validate auth
  if (isAuthValid(request)) {
    return NextResponse.next()
  }
 
  const loginUrl = new URL('/login', request.url)
  loginUrl.searchParams.set('from', request.nextUrl.pathname)
 
  return NextResponse.redirect(loginUrl)
}Edge API Routes
以前に Middleware を使用して外部 API にヘッダーを転送していた場合は、代わりに Edge API Routes を使用できます。
import { type NextRequest } from 'next/server'
 
export const config = {
  runtime: 'edge',
}
 
export default async function handler(req: NextRequest) {
  const authorization = req.cookies.get('authorization')
  return fetch('https://backend-api.com/api/protected', {
    method: req.method,
    headers: {
      authorization,
    },
    redirect: 'manual',
  })
}Cookies API の刷新
変更点の概要
| 追加された機能 | 削除されたもの | 
|---|---|
| cookies.set | cookie | 
| cookies.delete | clearCookie | 
| cookies.getWithOptions | cookies | 
説明
ベータ版でのフィードバックに基づき、NextRequest および NextResponse の Cookies API を、get/set モデルに合わせるように変更します。Cookies API は Map を拡張し、entries や values などのメソッドを含みます。
アップグレード方法
NextResponse には、以下の機能を持つ cookies インスタンスが追加されました。
- cookies.delete
- cookies.set
- cookies.getWithOptions
その他、Map からの拡張メソッドも利用できます。
変更前
import { NextResponse } from 'next/server'
import type { NextRequest } from 'next/server'
 
export function middleware(request: NextRequest) {
  // create an instance of the class to access the public methods. This uses `next()`,
  // you could use `redirect()` or `rewrite()` as well
  let response = NextResponse.next()
  // get the cookies from the request
  let cookieFromRequest = request.cookies['my-cookie']
  // set the `cookie`
  response.cookie('hello', 'world')
  // set the `cookie` with options
  const cookieWithOptions = response.cookie('hello', 'world', {
    path: '/',
    maxAge: 1000 * 60 * 60 * 24 * 7,
    httpOnly: true,
    sameSite: 'strict',
    domain: 'example.com',
  })
  // clear the `cookie`
  response.clearCookie('hello')
 
  return response
}変更後
export function middleware() {
  const response = new NextResponse()
 
  // set a cookie
  response.cookies.set('vercel', 'fast')
 
  // set another cookie with options
  response.cookies.set('nextjs', 'awesome', { path: '/test' })
 
  // get all the details of a cookie
  const { value, ...options } = response.cookies.getWithOptions('vercel')
  console.log(value) // => 'fast'
  console.log(options) // => { name: 'vercel', Path: '/test' }
 
  // deleting a cookie will mark it as expired
  response.cookies.delete('vercel')
 
  return response
}新しい User-Agent ヘルパー
変更点の概要
- ユーザーエージェントへのアクセスは、リクエストオブジェクトからは利用できなくなりました。
- Middleware のサイズを 17kb削減するために、新しいuserAgentヘルパーを追加しました。
説明
Middleware のサイズを削減するために、ユーザーエージェントをリクエストオブジェクトから抽出し、新しいヘルパー userAgent を作成しました。
このヘルパーは next/server からインポートされ、ユーザーエージェントの使用をオプトインできます。このヘルパーを使用すると、リクエストオブジェクトで利用可能だったのと同じプロパティにアクセスできます。
アップグレード方法
- next/serverから- userAgentヘルパーをインポートします。
- 使用したいプロパティを分割代入します。
変更前
import { NextRequest, NextResponse } from 'next/server'
 
export function middleware(request: NextRequest) {
  const url = request.nextUrl
  const viewport = request.ua.device.type === 'mobile' ? 'mobile' : 'desktop'
  url.searchParams.set('viewport', viewport)
  return NextResponse.rewrite(url)
}変更後
import { NextRequest, NextResponse, userAgent } from 'next/server'
 
export function middleware(request: NextRequest) {
  const url = request.nextUrl
  const { device } = userAgent(request)
  const viewport = device.type === 'mobile' ? 'mobile' : 'desktop'
  url.searchParams.set('viewport', viewport)
  return NextResponse.rewrite(url)
}Page Match Data の廃止
変更点の概要
- Middleware が特定のページマッチで呼び出されているかどうかを確認するには、URLPatternを使用します。
説明
現在、Middleware は Next.js のルートマニフェスト(内部構成)に基づいて、アセットまたは Page を提供しているかどうかを推定しています。この値は request.page を介して公開されていました。
ページとアセットのマッチングをより正確にするために、Web 標準の URLPattern API を使用するようになりました。
アップグレード方法
Middleware が特定のページマッチで呼び出されているかどうかを確認するには、URLPattern を使用します。
変更前
import { NextResponse } from 'next/server'
import type { NextRequest, NextFetchEvent } from 'next/server'
 
export function middleware(request: NextRequest, event: NextFetchEvent) {
  const { params } = event.request.page
  const { locale, slug } = params
 
  if (locale && slug) {
    const { search, protocol, host } = request.nextUrl
    const url = new URL(`${protocol}//${locale}.${host}/${slug}${search}`)
    return NextResponse.redirect(url)
  }
}変更後
import { NextResponse } from 'next/server'
import type { NextRequest } from 'next/server'
 
const PATTERNS = [
  [
    new URLPattern({ pathname: '/:locale/:slug' }),
    ({ pathname }) => pathname.groups,
  ],
]
 
const params = (url) => {
  const input = url.split('?')[0]
  let result = {}
 
  for (const [pattern, handler] of PATTERNS) {
    const patternResult = pattern.exec(input)
    if (patternResult !== null && 'pathname' in patternResult) {
      result = handler(patternResult)
      break
    }
  }
  return result
}
 
export function middleware(request: NextRequest) {
  const { locale, slug } = params(request.url)
 
  if (locale && slug) {
    const { search, protocol, host } = request.nextUrl
    const url = new URL(`${protocol}//${locale}.${host}/${slug}${search}`)
    return NextResponse.redirect(url)
  }
}内部 Next.js リクエストでの Middleware 実行
変更点の概要
- Middleware は _nextを含むすべてのリクエストで実行されます。
説明
Next.js v12.2 より前は、Middleware は _next リクエストでは実行されませんでした。
Middleware を認証に使用していた場合は、rewrite/redirect を使用して、認証エラー、ログインフォーム、または API Route を表示するページに移行する必要があります。
rewrite/redirect を使用して移行する方法の例については、レスポンスボディの廃止 を参照してください。
役に立ちましたか?