lenec ru

← все посты

Middleware в Next.js: где уместен, а где лучше edge-route или layout

17K

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-е страницы или конфиге?». Часто — можно, и это будет проще читать через полгода.

Комментарии 0

  • Будьте первым, кто оставит комментарий.

Войдите, чтобы оставить комментарий.