Middleware

Middleware позволяет выполнять код перед завершением запроса. Затем, на основе входящего запроса, вы можете изменить ответ, переписав его, перенаправив, изменив заголовки запроса или ответа, либо ответив напрямую.

Middleware выполняется до сопоставления кэшированного контента и маршрутов. Подробнее см. в разделе Сопоставление путей.

Варианты использования

Интеграция Middleware в ваше приложение может значительно улучшить производительность, безопасность и пользовательский опыт. Вот некоторые распространённые сценарии, где Middleware особенно эффективен:

  • Аутентификация и авторизация: проверка личности пользователя и сессионных куки перед предоставлением доступа к определённым страницам или API-маршрутам.
  • Перенаправления на стороне сервера (Server-Side Redirects): перенаправление пользователей на уровне сервера на основе определённых условий (например, локали или роли пользователя).
  • Перезапись путей (Path Rewriting): поддержка A/B-тестирования, постепенного внедрения функций или устаревших путей путём динамической перезаписи путей к API-маршрутам или страницам на основе свойств запроса.
  • Обнаружение ботов (Bot Detection): защита ресурсов путём обнаружения и блокировки бот-трафика.
  • Логирование и аналитика: сбор и анализ данных запроса для получения аналитических данных перед обработкой страницей или API.
  • Функциональные флаги (Feature Flagging): динамическое включение или отключение функций для плавного внедрения или тестирования.

Также важно понимать ситуации, когда Middleware может быть не оптимальным решением. Вот некоторые сценарии, на которые стоит обратить внимание:

  • Сложная выборка и обработка данных: Middleware не предназначен для прямой выборки или обработки данных — это следует делать в обработчиках маршрутов (Route Handlers) или серверных утилитах.
  • Ресурсоёмкие вычислительные задачи: Middleware должен быть лёгким и быстро отвечать, иначе это может вызвать задержки при загрузке страницы. Ресурсоёмкие задачи следует выполнять в выделенных обработчиках маршрутов.
  • Расширенное управление сессиями: хотя Middleware может выполнять базовые задачи управления сессиями, расширенное управление должно осуществляться специализированными сервисами аутентификации или в обработчиках маршрутов.
  • Прямые операции с базой данных: выполнение прямых операций с базой данных в Middleware не рекомендуется. Взаимодействие с базой данных должно происходить в обработчиках маршрутов или серверных утилитах.

Конвенция

Используйте файл middleware.ts (или .js) в корне вашего проекта для определения Middleware. Например, на том же уровне, что pages или app, или внутри src, если это применимо.

Примечание: Хотя поддерживается только один файл middleware.ts на проект, вы всё равно можете организовать логику Middleware модульно. Разделите функциональность Middleware на отдельные файлы .ts или .js и импортируйте их в основной файл middleware.ts. Это обеспечивает более чистое управление Middleware для конкретных маршрутов, агрегированных в middleware.ts для централизованного контроля. Ограничение одним файлом Middleware упрощает конфигурацию, предотвращает потенциальные конфликты и оптимизирует производительность, избегая нескольких слоёв Middleware.

Пример

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

// Эта функция может быть помечена как `async`, если внутри используется `await`
export function middleware(request: NextRequest) {
  return NextResponse.redirect(new URL('/home', request.url))
}

// Подробнее см. в разделе "Сопоставление путей"
export const config = {
  matcher: '/about/:path*',
}
import { NextResponse } from 'next/server'

// Эта функция может быть помечена как `async`, если внутри используется `await`
export function middleware(request) {
  return NextResponse.redirect(new URL('/home', request.url))
}

// Подробнее см. в разделе "Сопоставление путей"
export const config = {
  matcher: '/about/:path*',
}

Сопоставление путей

Middleware будет вызываться для каждого маршрута в вашем проекте. Учитывая это, важно использовать матчеры для точного таргетинга или исключения определённых маршрутов. Порядок выполнения следующий:

  1. headers из next.config.js
  2. redirects из next.config.js
  3. Middleware (rewrites, redirects и т.д.)
  4. beforeFiles (rewrites) из next.config.js
  5. Маршруты файловой системы (public/, _next/static/, pages/, app/ и т.д.)
  6. afterFiles (rewrites) из next.config.js
  7. Динамические маршруты (/blog/[slug])
  8. fallback (rewrites) из next.config.js

Есть два способа определить, для каких путей будет выполняться Middleware:

  1. Пользовательская конфигурация matcher
  2. Условные выражения

Matcher

matcher позволяет фильтровать Middleware для выполнения на определённых путях.

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

Вы можете сопоставить один путь или несколько путей с помощью синтаксиса массива:

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

Конфигурация matcher поддерживает полные регулярные выражения, включая негативный просмотр вперёд (negative lookaheads) или сопоставление символов. Пример негативного просмотра вперёд для сопоставления всех путей, кроме определённых:

middleware.js
export const config = {
  matcher: [
    /*
     * Сопоставить все пути запросов, кроме начинающихся с:
     * - api (API-маршруты)
     * - _next/static (статические файлы)
     * - _next/image (файлы оптимизации изображений)
     * - favicon.ico (файл фавикона)
     */
    '/((?!api|_next/static|_next/image|favicon.ico).*)',
  ],
}

Вы также можете обойти Middleware для определённых запросов, используя массивы missing или has, или их комбинацию:

middleware.js
export const config = {
  matcher: [
    /*
     * Сопоставить все пути запросов, кроме начинающихся с:
     * - api (API-маршруты)
     * - _next/static (статические файлы)
     * - _next/image (файлы оптимизации изображений)
     * - favicon.ico (файл фавикона)
     */
    {
      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' }],
    },
  ],
}

Полезно знать: Значения matcher должны быть константами, чтобы их можно было статически проанализировать во время сборки. Динамические значения, такие как переменные, будут проигнорированы.

Настроенные матчеры:

  1. ДОЛЖНЫ начинаться с /
  2. Могут включать именованные параметры: /about/:path соответствует /about/a и /about/b, но не /about/a/c
  3. Могут иметь модификаторы для именованных параметров (начинающихся с :): /about/:path* соответствует /about/a/b/c, потому что * означает ноль или более. ? означает ноль или один, а +один или более
  4. Могут использовать регулярные выражения, заключённые в скобки: /about/(.*) эквивалентно /about/:path*

Подробнее см. в документации path-to-regexp.

Полезно знать: Для обратной совместимости Next.js всегда рассматривает /public как /public/index. Поэтому матчер /public/:path будет соответствовать.

Условные выражения

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))
  }
}
import { NextResponse } from 'next/server'

export function middleware(request) {
  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

API NextResponse позволяет:

  • redirect: перенаправить входящий запрос на другой URL
  • rewrite: переписать ответ, отображая заданный URL
  • Устанавливать заголовки запросов для API-маршрутов, getServerSideProps и направлений перезаписи
  • Устанавливать куки ответа
  • Устанавливать заголовки ответа

Для формирования ответа из Middleware вы можете:

  1. Использовать rewrite для маршрута (Страница или Edge API Route), который формирует ответ
  2. Вернуть NextResponse напрямую. См. Формирование ответа

Использование куки

Куки — это обычные заголовки. В Request они хранятся в заголовке Cookie. В Response они находятся в заголовке Set-Cookie. Next.js предоставляет удобный способ доступа и управления этими куки через расширение cookies в NextRequest и NextResponse.

  1. Для входящих запросов cookies предоставляет следующие методы: get, getAll, set и delete. Вы можете проверить наличие куки с помощью has или удалить все куки с помощью clear.
  2. Для исходящих ответов cookies предоставляет методы get, getAll, set и delete.
import { NextResponse } from 'next/server'
import type { NextRequest } from 'next/server'

export function middleware(request: NextRequest) {
  // Предположим, что во входящем запросе присутствует заголовок "Cookie:nextjs=fast"
  // Получение куки из запроса с помощью API `RequestCookies`
  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

  // Установка куки в ответе с помощью API `ResponseCookies`
  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: '/' }
  // Исходящий ответ будет содержать заголовок `Set-Cookie:vercel=fast;path=/`.

  return response
}
import { NextResponse } from 'next/server'

export function middleware(request) {
  // Предположим, что во входящем запросе присутствует заголовок "Cookie:nextjs=fast"
  // Получение куки из запроса с помощью API `RequestCookies`
  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

  // Установка куки в ответе с помощью API `ResponseCookies`
  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: '/' }
  // Исходящий ответ будет содержать заголовок `Set-Cookie:vercel=fast;path=/test`.

  return response
}

Установка заголовков

Вы можете устанавливать заголовки запросов и ответов с помощью API NextResponse (установка заголовков запросов доступна начиная с Next.js v13.0.0).

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

export function middleware(request: NextRequest) {
  // Клонируем заголовки запроса и устанавливаем новый заголовок `x-hello-from-middleware1`
  const requestHeaders = new Headers(request.headers)
  requestHeaders.set('x-hello-from-middleware1', 'hello')

  // Вы также можете установить заголовки запроса в NextResponse.rewrite
  const response = NextResponse.next({
    request: {
      // Новые заголовки запроса
      headers: requestHeaders,
    },
  })

  // Устанавливаем новый заголовок ответа `x-hello-from-middleware2`
  response.headers.set('x-hello-from-middleware2', 'hello')
  return response
}
import { NextResponse } from 'next/server'

export function middleware(request) {
  // Клонируем заголовки запроса и устанавливаем новый заголовок `x-hello-from-middleware1`
  const requestHeaders = new Headers(request.headers)
  requestHeaders.set('x-hello-from-middleware1', 'hello')

  // Вы также можете установить заголовки запроса в NextResponse.rewrite
  const response = NextResponse.next({
    request: {
      // Новые заголовки запроса
      headers: requestHeaders,
    },
  })

  // Устанавливаем новый заголовок ответа `x-hello-from-middleware2`
  response.headers.set('x-hello-from-middleware2', 'hello')
  return response
}

Полезно знать: Избегайте установки больших заголовков, так как это может вызвать ошибку 431 Request Header Fields Too Large в зависимости от конфигурации вашего сервера.

CORS

Вы можете настроить заголовки CORS в Middleware для разрешения кросс-доменных запросов, включая простые и предварительные запросы.

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) {
  // Проверяем источник запроса
  const origin = request.headers.get('origin') ?? ''
  const isAllowedOrigin = allowedOrigins.includes(origin)

  // Обрабатываем предварительные запросы
  const isPreflight = request.method === 'OPTIONS'

  if (isPreflight) {
    const preflightHeaders = {
      ...(isAllowedOrigin && { 'Access-Control-Allow-Origin': origin }),
      ...corsOptions,
    }
    return NextResponse.json({}, { headers: preflightHeaders })
  }

  // Обрабатываем простые запросы
  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*',
}
import { 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) {
  // Проверяем источник запроса
  const origin = request.headers.get('origin') ?? ''
  const isAllowedOrigin = allowedOrigins.includes(origin)

  // Обрабатываем предварительные запросы
  const isPreflight = request.method === 'OPTIONS'

  if (isPreflight) {
    const preflightHeaders = {
      ...(isAllowedOrigin && { 'Access-Control-Allow-Origin': origin }),
      ...corsOptions,
    }
    return NextResponse.json({}, { headers: preflightHeaders })
  }

  // Обрабатываем простые запросы
  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*',
}

Формирование ответа

Вы можете напрямую отвечать из Middleware, возвращая экземпляр Response или NextResponse. (Доступно начиная с Next.js v13.1.0)

import { NextRequest } from 'next/server'
import { isAuthenticated } from '@lib/auth'

// Ограничиваем middleware путями, начинающимися с `/api/`
export const config = {
  matcher: '/api/:function*',
}

export function middleware(request: NextRequest) {
  // Вызываем функцию аутентификации для проверки запроса
  if (!isAuthenticated(request)) {
    // Возвращаем JSON с сообщением об ошибке
    return Response.json(
      { success: false, message: 'Ошибка аутентификации' },
      { status: 401 }
    )
  }
}
import { isAuthenticated } from '@lib/auth'

// Ограничиваем middleware путями, начинающимися с `/api/`
export const config = {
  matcher: '/api/:function*',
}

export function middleware(request) {
  // Вызываем функцию аутентификации для проверки запроса
  if (!isAuthenticated(request)) {
    // Возвращаем JSON с сообщением об ошибке
    return Response.json(
      { success: false, message: 'Ошибка аутентификации' },
      { status: 401 }
    )
  }
}

waitUntil и NextFetchEvent

Объект NextFetchEvent расширяет нативный FetchEvent и включает метод waitUntil().

Метод waitUntil() принимает промис в качестве аргумента и продлевает время жизни Middleware до завершения промиса. Это полезно для выполнения фоновых задач.

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()
}

Расширенные флаги Middleware

В версии v13.1 Next.js были добавлены два дополнительных флага для middleware: skipMiddlewareUrlNormalize и skipTrailingSlashRedirect для обработки сложных сценариев.

skipTrailingSlashRedirect отключает редиректы Next.js для добавления или удаления завершающих слешей. Это позволяет кастомно обрабатывать завершающие слеши внутри middleware, что упрощает постепенные миграции.

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()
  }

  // Применяем обработку завершающего слеша
  if (
    !pathname.endsWith('/') &&
    !pathname.match(/((?!\.well-known(?:\/.*)?)(?:[^/]+\/)*[^/]+\.\w+)/)
  ) {
    req.nextUrl.pathname += '/'
    return NextResponse.redirect(req.nextUrl)
  }
}

skipMiddlewareUrlNormalize позволяет отключить нормализацию URL в Next.js для одинаковой обработки прямых переходов и клиентских переходов. В сложных случаях этот параметр дает полный контроль, используя оригинальный 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)
  // С этим флагом будет /_next/data/build-id/hello.json
  // Без флага нормализовалось бы до /hello
}

Среда выполнения (Runtime)

Middleware в настоящее время поддерживает только Edge runtime. Среда выполнения Node.js не поддерживается.

История версий

ВерсияИзменения
v13.1.0Добавлены расширенные флаги Middleware
v13.0.0Middleware может изменять заголовки запросов, заголовки ответов и отправлять ответы
v12.2.0Middleware стал стабильным, см. руководство по обновлению
v12.0.9Принудительное использование абсолютных URL в Edge Runtime (PR)
v12.0.0Добавлен Middleware (бета-версия)