Middleware в Next.js: где уместен, а где лучше edge-route или layout
Middleware в Next.js — одна из тех штук, которыми сначала хочется решить всё. Авторизация — middleware. Редирект — middleware. A/B-тесты — middleware. Логирование — middleware. Через полгода ты обнаруживаешь, что у тебя в одном файле 600 строк условий, и каждый запрос проходит через все.
Расскажу, какие задачи middleware реально решает хорошо, а какие лучше отдать другим механизмам — layout, route handler, серверному компоненту или edge-функции.
Что такое middleware в Next.js
Файл middleware.ts в корне проекта. Запускается перед каждым запросом, который попадает под matcher. Работает на edge runtime (V8 isolate, без полного Node API).
import { NextResponse } from "next/server";
import type { NextRequest } from "next/server";
export function middleware(req: NextRequest) {
if (!req.cookies.get("session")) {
return NextResponse.redirect(new URL("/login", req.url));
}
return NextResponse.next();
}
export const config = {
matcher: ["/dashboard/:path*", "/account/:path*"],
};
Важно понимать: middleware выполняется на каждый запрос к матчингу, включая запросы за статикой и за _next/data. Поэтому всё, что туда попадает, — критично по перформансу.
Что middleware делает хорошо
1. Редиректы по URL и по куки
Самый чистый сценарий. Незалогиненного пользователя со страниц аккаунта — на /login. С домена old.example.com — на example.com. Эти штуки работают на уровне HTTP-редиректа, до запуска приложения. Быстро, дёшево.
2. Локализация по Accept-Language
Определить язык пользователя и редиректить на /ru/... или /en/.... Тут middleware идеален: проверяешь cookie или заголовок, делаешь rewrite на нужный prefix.
export function middleware(req: NextRequest) {
const locale = req.cookies.get("locale")?.value
?? detectFromHeader(req.headers.get("accept-language"))
?? "ru";
if (!req.nextUrl.pathname.startsWith(`/${locale}`)) {
return NextResponse.redirect(
new URL(`/${locale}${req.nextUrl.pathname}`, req.url),
);
}
}
3. A/B-тесты на уровне роутинга
Когда тебе нужно показать одну группу пользователей одну версию страницы, другую — другую. В middleware ты выбираешь вариант (по cookie или по hash от user-id), пишешь его в cookie, делаешь rewrite на нужную страницу. Дальше — обычный рендер.
4. Простой rate limit
Не ультимативный, не как у профессионального API gateway, но базовый — да. Хранить счётчики в Redis (через REST), отдавать 429 при превышении. Делается в десяток строк.
Что middleware НЕ должен делать
1. Полную логику авторизации с проверкой ролей
Middleware видит, есть ли cookie. Дальше нужно проверить роль, права, конкретные ресурсы. Это уже не «дёрни кеш и реши» — это полноценный запрос к API. На каждом обращении ко всему сайту.
Я делаю в middleware только проверку «залогинен или нет» (по факту наличия валидной cookie). Дальше — на странице или в route handler:
// app/dashboard/page.tsx
import { redirect } from "next/navigation";
import { getCurrentUser } from "@/auth";
export default async function DashboardPage() {
const user = await getCurrentUser();
if (!user) redirect("/login");
if (!user.canAccessDashboard) redirect("/no-access");
return <Dashboard user={user} />;
}
Так каждая страница сама знает, что ей нужно. Middleware только не пускает совсем чужих.
2. Тяжёлые вычисления и большие payloads
На edge runtime есть лимит на размер кода функции. Если ты тащишь туда библиотеку для парсинга URL весом 200 КБ — получишь ошибку при деплое или, что хуже, медленные cold starts.
3. Запросы к базе
На edge runtime нет TCP-соединений к Postgres напрямую (без специальных адаптеров). Можно через REST-прокси, но это всё равно дополнительный сетевой хоп на каждый запрос. Лучше делать запросы в самой странице или route handler — там уже есть контекст и кеш.
4. Логирование с сохранением в файл/БД
Логи — да, но через push к внешнему сервису (Datadog, Logtail, Sentry breadcrumbs). Без локального состояния. Если хочешь подробное логирование — лучше на бэке за middleware.
Альтернативы под конкретные задачи
Защита роутов: layout
Если у тебя в /admin все страницы должны быть только для админов — оборачиваешь их в общий layout с проверкой:
// app/admin/layout.tsx
import { getCurrentUser } from "@/auth";
import { redirect } from "next/navigation";
export default async function AdminLayout({ children }: { children: React.ReactNode }) {
const user = await getCurrentUser();
if (!user?.isAdmin) redirect("/");
return <>{children}</>;
}
Layout рендерится один раз, проверка одна, без дублирования в каждой странице. Middleware тут не нужен.
Защита API: route handler с auth
В route handlers вместо middleware пиши простую утилиту-обёртку:
// app/api/admin/users/route.ts
import { requireAdmin } from "@/auth";
export async function GET(req: Request) {
const user = await requireAdmin(req);
if (user instanceof Response) return user; // 401/403
return Response.json(await db.users.findMany());
}
Это явный код. На ревью видно, что роут защищён. Middleware с условиями по путям — менее очевиден.
Редиректы при изменении URL: redirects в next.config.js
Если у тебя простые однозначные редиректы (без проверки cookie), они задаются в конфиге, не в middleware:
module.exports = {
async redirects() {
return [
{ source: "/old-path", destination: "/new-path", permanent: true },
];
},
};
Это работает на уровне сборки, не требует выполнения функции на каждый запрос. Дешевле.
Заголовки безопасности: headers в next.config.js
CSP, X-Frame-Options, Permissions-Policy — задавай в next.config.js через headers(). Это тот же подход — задаётся один раз, применяется на уровне платформы.
Грабли, которые я ловила
1. Бесконечный редирект
Самая частая. Middleware редиректит с /foo на /login, но /login попадает под matcher, и снова редирект. Лечится либо точным matcher (исключить /login), либо проверкой req.nextUrl.pathname внутри функции.
2. Middleware на API-роутах
Если matcher включает /api, middleware влияет на API-вызовы. Иногда это нужно (rate limit), иногда — катастрофа (CORS, длинные запросы). Я обычно явно исключаю API в matcher или прописываю условие.
3. Cookie не доходит
Middleware ставит cookie через res.cookies.set, но клиент её не видит — потому что middleware вернул rewrite, и Next теряет cookies при дальнейшей цепочке. Решение — ставить cookie на самом ответе после rewrite, не до.
4. Edge runtime не может всё
Половина библиотек, которые работают в Node, не работают в edge. Если ты тащишь сложный JWT-парсер — может оказаться, что он использует Node-specific API. Решение — либо находить edge-совместимую альтернативу (например, jose для JWT), либо переносить логику из middleware в route handler с Node-runtime.
Производительность
Middleware выполняется на каждом матче. Если matcher — /((?!_next/static|_next/image|favicon.ico).*), это все запросы кроме статики. На крупном сайте — десятки тысяч в минуту.
Что я измеряю на каждом релизе:
- Время выполнения middleware (P95). Должно быть до 5–10 мс.
- Cold start (на edge холодный старт быстрее, но всё равно есть).
- Количество исходящих запросов из middleware. Если на каждый запрос ходим в Redis — это удваивает latency.
На одном проекте я случайно затащила в middleware lookup в API за деталями пользователя. P95 пользователей вырос на 80 мс. Не критично, но через две недели я это нашла и вынесла обратно в layout.
Что запомнить
Middleware — не «общий перехватчик всех запросов». Это инструмент для редиректов, локализации, A/B-тестов и базовой проверки авторизации. Всё, что требует доступа к БД, сложной логики или тяжёлых библиотек, — выноси в layout или route handler.
Перед тем как добавить новое условие в middleware, спроси себя: «А нельзя ли это решить в layout-е страницы или конфиге?». Часто — можно, и это будет проще читать через полгода.