Middleware в Next.js 16: полное руководство по аутентификации и защите маршрутов (2026)

Полный гайд по middleware в Next.js 16: настройка matcher, аутентификация через JWT-cookies, выбор между Edge и Node.js runtime, работа с заголовками и типичные ошибки при переходе с Pages Router.

Middleware в Next.js 16: auth-гайд 2026

Обновлено: 5 июля 2026

Middleware в Next.js 16 — это код, который выполняется до завершения обработки запроса и позволяет модифицировать ответ через переписывания, редиректы, изменение заголовков и работу с cookies. С момента выпуска Next.js 15.2 middleware может работать не только в Edge Runtime, но и в стабильном Node.js runtime, что открывает возможность использовать полноценные Node-библиотеки, включая драйверы баз данных и bcrypt, прямо в файле middleware.ts. В этом руководстве я покажу, как настроить middleware для аутентификации, ограничить его конкретными маршрутами через matcher и избежать типичных ошибок, с которыми сталкиваются разработчики при переходе с Pages Router.

  • Middleware в Next.js 16 живёт в единственном файле middleware.ts в корне проекта или в директории src/ и выполняется для каждого совпадающего запроса.
  • С Next.js 15.2 Node.js runtime стабилен: включите его через export const config = { runtime: 'nodejs' }, чтобы использовать полноценные npm-пакеты.
  • Свойство matcher ограничивает выполнение конкретными маршрутами и снижает нагрузку. Избегайте регулярного выражения по умолчанию, которое ловит всё.
  • Для аутентификации читайте cookies через request.cookies.get(), а редиректите неавторизованных пользователей вызовом NextResponse.redirect(new URL('/login', request.url)).
  • Middleware не подходит для тяжёлой бизнес-логики. Сложные проверки прав вынесите в Server Components или Server Actions, чтобы избежать замедления TTFB.

Что такое middleware в Next.js 16

Если совсем коротко, middleware это функция, которая перехватывает входящий HTTP-запрос ещё до того, как Next.js передаст его нужному route handler, серверному компоненту или статическому файлу. В отличие от API Routes или Server Actions, middleware не возвращает данные пользователю напрямую: он либо разрешает запросу продолжить путь, либо переписывает URL, либо возвращает редирект, либо добавляет заголовки к ответу. Я работаю с этой абстракцией с версии 12, когда её только представили как экспериментальную, и, честно говоря, в 16 версии она наконец достигла зрелости благодаря стабильному Node.js runtime.

Внутри middleware вы получаете два ключевых объекта: NextRequest (расширение стандартного Request с методами .cookies, .nextUrl, .geo) и NextResponse, фабрику ответов, через которую и происходит вся магия. Типовые сценарии: защита приватных разделов (панель администратора, личный кабинет), A/B-тестирование через переписывание URL, i18n-редиректы на основе Accept-Language, добавление CSP-заголовков и логирование запросов. Всё это до того, как рендерится хоть один React-компонент.

Базовая настройка middleware.ts

Middleware активируется автоматически, если в корне проекта (или в src/, если вы используете эту структуру) существует файл middleware.ts. Не нужно ничего регистрировать в next.config.js, сам факт наличия файла достаточен. Вот минимальный рабочий пример, который добавляет заголовок x-app-version ко всем ответам:

// middleware.ts
import { NextResponse } from 'next/server'
import type { NextRequest } from 'next/server'

export function middleware(request: NextRequest) {
  const response = NextResponse.next()
  response.headers.set('x-app-version', '2026.07.05')
  return response
}

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

Здесь NextResponse.next() означает «пропусти запрос дальше, но с моими изменениями». Если бы я хотел заблокировать запрос, я бы вернул new NextResponse('Forbidden', { status: 403 }). Регулярное выражение в matcher исключает системные пути Next.js, чтобы middleware не выполнялся для статических ассетов и внутренних API-роутов. Это первое, о чём забывают новички и получают лишние 20–40 мс к TTFB на каждой картинке.

Если вы приходите из Pages Router, помните: старый формат _middleware.ts внутри директории pages больше не поддерживается уже несколько версий. Всё живёт в одном корневом файле, а исключения в виде «per-route middleware» реализуются через ветвление внутри одной функции.

Как ограничить middleware конкретными маршрутами

Свойство matcher, наверное, самое недооценённое поле в конфигурации middleware. Оно принимает строку или массив строк с path-паттернами и определяет, для каких URL функция вообще будет выполняться. Всё остальное Next.js статически анализирует на этапе сборки и не тратит время на проверку в рантайме. Правильно настроенный matcher это разница между 5 мс и 50 мс дополнительной задержки на страницу.

Три поддерживаемых формата: обычный путь (/dashboard), путь с параметрами (/dashboard/:path*) и полноценное regex-выражение с отрицательными lookahead-ами. Вот как я обычно защищаю раздел /dashboard и всё, что под ним, но пропускаю /dashboard/public:

// middleware.ts
export const config = {
  matcher: [
    // Всё под /dashboard, кроме /dashboard/public
    {
      source: '/dashboard/:path*',
      missing: [{ type: 'header', key: 'x-skip-auth' }],
    },
    '/admin/:path*',
    '/api/private/:path*',
  ],
}

Массив вместо строки позволяет комбинировать несколько независимых паттернов. Условные matcher-ы через has, missing и value добавляют проверку заголовков, cookies и query-параметров ещё до вызова функции, и это самый эффективный способ отсечь ненужные срабатывания. Например, можно запускать middleware только для запросов без определённого cookie, что удобно для onboarding-флоу.

Как настроить middleware для аутентификации

Аутентификация это, пожалуй, 80% случаев, ради которых middleware вообще существует. Идея простая: middleware читает cookie сессии, проверяет его валидность и либо пропускает запрос, либо редиректит на /login. В связке с Server Actions для мутаций форм вы получаете сквозную систему: логин через Server Action ставит cookie, middleware защищает страницы, а сами страницы уже серверные компоненты, которым не нужно повторять проверку.

Вот рабочий пример с JWT-cookie и библиотекой jose (она работает и в Edge, и в Node runtime):

// middleware.ts
import { NextResponse } from 'next/server'
import type { NextRequest } from 'next/server'
import { jwtVerify } from 'jose'

const SECRET = new TextEncoder().encode(process.env.AUTH_SECRET!)

export async function middleware(request: NextRequest) {
  const token = request.cookies.get('session')?.value

  if (!token) {
    return redirectToLogin(request)
  }

  try {
    const { payload } = await jwtVerify(token, SECRET)
    const response = NextResponse.next()
    // Пробрасываем userId в заголовки для серверных компонентов
    response.headers.set('x-user-id', String(payload.sub))
    return response
  } catch {
    return redirectToLogin(request)
  }
}

function redirectToLogin(request: NextRequest) {
  const loginUrl = new URL('/login', request.url)
  loginUrl.searchParams.set('from', request.nextUrl.pathname)
  return NextResponse.redirect(loginUrl)
}

export const config = {
  matcher: ['/dashboard/:path*', '/settings/:path*', '/billing/:path*'],
}

Обратите внимание на два приёма. Первый: параметр from в URL логина. После успешной авторизации я редиректю обратно на исходную страницу. Второй: заголовок x-user-id. Middleware не может напрямую передавать данные в серверные компоненты (это две разные среды выполнения), но заголовки, как оказалось, надёжный канал. В серверном компоненте я потом читаю его через headers().get('x-user-id'). В одном из последних проектов я вначале пробовал прокидывать userId через query-параметры, и это сломалось на первой же клиентской навигации, так что заголовки тут единственный вменяемый вариант.

Node.js runtime против Edge Runtime

До версии 15.2 middleware мог работать только в Edge Runtime, это лёгкая среда на базе V8 Isolates без доступа к Node API. Такие ограничения не давали использовать многие npm-пакеты: не работали bcrypt, драйверы Postgres и Mongo, fs и десятки других модулей. Приходилось выносить проверки в отдельные API-роуты. Начиная с Next.js 15.2 Node.js runtime для middleware стал стабильным, а в 16 версии это уже поведение по умолчанию для новых проектов.

Включается runtime одной строкой в конфиге:

// middleware.ts
export const config = {
  runtime: 'nodejs', // 'nodejs' | 'edge'
  matcher: ['/dashboard/:path*'],
}
ХарактеристикаEdge RuntimeNode.js Runtime
Холодный старт~5–15 мс~50–150 мс
Доступ к npm-пакетамОграниченный (только Web APIs)Полный доступ
Драйверы БД (pg, mongodb)Не работают напрямуюРаботают
bcrypt / crypto Node APIТолько WebCryptoПолный набор
Максимальный размер бандла1 МБ (жёсткий лимит)Без лимита
Развёртывание на VercelБлижайшая edge-локацияРегион функций
Подходит дляA/B-тесты, i18n, лёгкая authТяжёлая auth, обращения к БД

Мой опыт: если вы валидируете JWT через jose, используйте Edge, это быстрее. Если нужен bcrypt для проверки пароля прямо в middleware (плохая идея, но иногда возникает), переходите на Node. В большинстве случаев граница проходит по признаку «использую ли я нативный Node-модуль»: если да, то Node runtime; если только Web-стандарты, то Edge. Дополнительные технические подробности по спецификации, которую использует Edge Runtime, есть в документации MDN по Fetch API.

Работа с cookies и заголовками

API для cookies в middleware отличается от того, что вы видите в Server Components. Здесь работа идёт через объекты запроса и ответа напрямую: request.cookies для чтения, response.cookies для записи. Разделение важное, вы не можете «записать» cookie в запрос и ожидать, что клиент её увидит.

// middleware.ts
import { NextResponse } from 'next/server'
import type { NextRequest } from 'next/server'

export function middleware(request: NextRequest) {
  // Чтение
  const theme = request.cookies.get('theme')?.value ?? 'light'
  const consent = request.cookies.has('cookie-consent')

  const response = NextResponse.next()

  // Запись
  if (!consent) {
    response.cookies.set({
      name: 'visited',
      value: 'true',
      httpOnly: true,
      sameSite: 'lax',
      maxAge: 60 * 60 * 24 * 30, // 30 дней
    })
  }

  // Удаление
  if (theme === 'invalid') {
    response.cookies.delete('theme')
  }

  return response
}

С заголовками похожая история. Читать входящие можно через request.headers.get('user-agent'). Модифицировать исходящие через response.headers.set(). Особый случай это модификация входящих заголовков, которые увидят серверные компоненты. Делается через второй аргумент NextResponse.next({ request: { headers: newHeaders } }). Пример из гайда по директиве use cache: я иногда прокидываю x-request-id в заголовки запроса, чтобы кэшируемые компоненты могли его логировать.

Частые ошибки и как их избежать

За четыре года работы с middleware я собрал коллекцию граблей. Три самых частых.

1. Слишком широкий matcher

Дефолтный matcher из документации '/((?!api|_next/static|_next/image|favicon.ico).*)' хорош как отправная точка, но в продакшене он ловит все страницы, включая маркетинговый лендинг, где аутентификация не нужна. Явно перечисляйте защищённые пути.

2. Async без обработки ошибок

Если ваша middleware-функция асинхронная и внутри есть fetch или обращение к БД, любое исключение приведёт к 500-й ошибке для пользователя. Оборачивайте всё в try/catch, а в блоке catch возвращайте NextResponse.next(), это «безопасный дефолт». Я поймал такую ситуацию в проде на выходных, когда база данных на секунду отвалилась, и пятнадцать минут вся авторизация возвращала 500. Больше повторять не хочется.

3. Попытка получить body запроса

Middleware не может читать тело POST-запроса, это ограничение архитектуры, и оно не изменится. Если нужно проверить payload, делайте это в Route Handler или Server Action.

Часто задаваемые вопросы

Можно ли использовать несколько файлов middleware в одном проекте?

Нет. Next.js поддерживает только один файл middleware.ts в корне проекта (или в src/). Если вам нужна разная логика для разных разделов, используйте ветвление внутри одной функции по request.nextUrl.pathname или разбейте логику на импортируемые модули.

Как получить доступ к базе данных из middleware?

Переключитесь на Node.js runtime через export const config = { runtime: 'nodejs' }. После этого можно использовать pg, mongodb, Prisma и любые другие драйверы. Но помните: обращения к БД в middleware добавляют задержку к каждому запросу, и лучше кэшировать проверку сессии в подписанном cookie.

В чём разница между middleware и Route Handlers?

Middleware выполняется до маршрутизации и не возвращает данные клиенту, он либо пропускает запрос, либо редиректит, либо переписывает URL. Route Handlers (файлы route.ts) это полноценные API-эндпоинты, которые обрабатывают запрос и возвращают JSON или HTML. Middleware работает для всех маршрутов, Route Handler только для своего.

Влияет ли middleware на статическую генерацию (SSG)?

Да, любой запрос, попадающий под matcher, обрабатывается динамически, и статический HTML не отдаётся из CDN без выполнения middleware. Это одна из причин точно настраивать matcher: страницы, для которых middleware не нужен, продолжат отдаваться как чистая статика.

Работает ли middleware с Partial Prerendering?

Да, middleware полностью совместим с PPR в Next.js 16. Статическая оболочка страницы отдаётся сразу, динамические части рендерятся после, а middleware выполняется в самом начале и может влиять на оба этапа через заголовки и cookies.

Ben Howard
Об авторе Ben Howard

Full-stack Next.js developer who's been with the framework since pages-only days. Slowly warming up to App Router.