Middleware в Next.js 16: полное руководство по аутентификации и защите маршрутов (2026)
Полный гайд по middleware в Next.js 16: настройка matcher, аутентификация через JWT-cookies, выбор между Edge и Node.js runtime, работа с заголовками и типичные ошибки при переходе с Pages Router.
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 версии это уже поведение по умолчанию для новых проектов.
Мой опыт: если вы валидируете 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.