Обсудить задачу

Next.js Middleware: геолокация и авторизация на edge

Next.js Middleware - это код, который выполняется на edge до того, как запрос доходит до кеша или роутинга приложения: он может редиректить пользователя по стране, проверять cookie авторизации и переписывать заголовки, не дожидаясь рендеринга страницы. Единственный файл middleware.ts в корне проекта перехватывает совпавшие запросы раньше любого React-компонента. Правильный matcher ограничивает его конкретными путями, поэтому лишняя нагрузка не возникает даже при высоком трафике.

В проектах веб-разработки мы регулярно видим одну и ту же ошибку: геолокацию или проверку авторизации реализуют на клиенте - через useEffect и редирект в браузере уже после того, как страница отрисовалась. Пользователь на долю секунды видит лендинг не на том языке или защищённый раздел, которого не должен видеть, и только потом происходит редирект. Next.js Middleware решает обе задачи на уровне запроса, до первой отрисовки. Дальше - как это работает, чем отличается поведение на Vercel и на альтернативных хостингах, и что реально влияет на производительность.

Для SaaS с международной аудиторией и B2B-лендингов с закрытыми разделами эта проблема ощутима сразу. Продуктовая команда показывает цену в USD пользователю из Германии, где ожидают EUR, и теряет часть конверсии на несоответствии. Или админ-панель на миллисекунду мелькает у неавторизованного посетителя до того, как клиентский код успевает его редиректить. Обе ситуации - следствие того, что решение принимается слишком поздно, уже в браузере.

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

Что выполняется до рендеринга: место Middleware в жизненном цикле запроса

Middleware перехватывает запрос раньше, чем сработает статическая генерация, ISR-кеш или сопоставление маршрута. Функция объявляется в единственном файле middleware.ts (или .js) в корне проекта или в src/, и Next.js вызывает её для каждого запроса, который попадает под matcher. Подробное описание жизненного цикла есть в официальной документации Next.js по Middleware.

Исторически Middleware выполнялся только в Edge Runtime - облегчённой среде без доступа к большинству модулей Node.js: нет прямых TCP-соединений к базе данных, ограничен набор API. Начиная с Next.js 15.2 появилась экспериментальная поддержка Node.js Middleware, что расширяет доступные API, но статус остаётся experimental, и на большинстве хостингов, кроме Vercel, поддержка ещё неполная.

Внутри Middleware есть доступ к NextRequest, и вы возвращаете NextResponse - редирект, rewrite, ответ с изменёнными заголовками или просто NextResponse.next(), чтобы пропустить запрос дальше без изменений. Middleware - это отдельный уровень от архитектуры рендеринга (Server Components и подходы вроде Astro Islands или React Server Components): он работает до неё, а не вместо неё.

Пример: минимальный middleware.ts
import { NextResponse } from 'next/server';
import type { NextRequest } from 'next/server';

export function middleware(request: NextRequest) {
  return NextResponse.next();
}

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

Геолокация: персонализация и редиректы по стране

Геолокация в Middleware определяет страну, регион и город пользователя по IP-адресу на уровне edge, до того как запрос дойдёт до страницы. На Vercel это делается через хелпер geolocation() из @vercel/functions или напрямую через заголовки x-vercel-ip-country, x-vercel-ip-country-region, x-vercel-ip-city, которые Vercel добавляет к каждому запросу.

Важная деталь: в более ранних версиях Next.js геолокация была доступна через request.geo прямо на объекте NextRequest. В актуальных версиях это поле убрано из типов, и официально рекомендованный путь - хелпер geolocation() либо чтение заголовков напрямую. Если вы видите request.geo в старом гайде, это устаревший API.

Ключевая оговорка про хостинг: заголовки x-vercel-ip-* появляются только на инфраструктуре Vercel. При деплое через Cloudflare, self-hosted Node-сервер или другой edge-провайдер геолокацию нужно получать иначе - например, через заголовок CF-IPCountry на Cloudflare, или через сторонний IP-geolocation сервис, если хостинг вообще не отдаёт эти данные. Код, который читает только x-vercel-ip-country, молча перестаёт работать при переезде с Vercel на другую платформу, и это стоит проверять до, а не после миграции хостинга.

Пример: редирект по стране в middleware.ts
import { NextResponse } from 'next/server';
import { geolocation } from '@vercel/functions';
import type { NextRequest } from 'next/server';

export function middleware(request: NextRequest) {
  const { country } = geolocation(request);
  const url = request.nextUrl;

  if (country === 'DE' && !url.pathname.startsWith('/de')) {
    url.pathname = `/de${url.pathname}`;
    return NextResponse.redirect(url);
  }

  return NextResponse.next();
}

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

Для мультиязычных сайтов геолокационный редирект стоит комбинировать с корректной hreflang-разметкой, иначе Google индексирует не ту языковую версию, на которую редиректит Middleware.

Авторизация: защита маршрутов без вспышки закрытого контента

Flash of protected content - ситуация, когда неавторизованный пользователь на короткое время видит защищённую страницу или её разметку, прежде чем клиентский код успевает его редиректить. Middleware устраняет эту проблему: проверка cookie сессии или JWT происходит на edge, до того как сервер вообще начнёт рендерить защищённую страницу.

Логика простая: Middleware читает cookie (например, session или token), опционально проверяет подпись JWT, и если пользователь не авторизован - редиректит на /login до рендеринга. Полную верификацию подписи JWT в Edge Runtime стоит делать библиотекой, совместимой с Web Crypto API (jose, а не jsonwebtoken, который зависит от Node.js-модулей).

Пример: проверка cookie авторизации в middleware.ts
import { NextResponse } from 'next/server';
import type { NextRequest } from 'next/server';

const PROTECTED_PATHS = ['/dashboard', '/settings', '/admin'];

export function middleware(request: NextRequest) {
  const { pathname } = request.nextUrl;
  const isProtected = PROTECTED_PATHS.some((path) => pathname.startsWith(path));

  if (!isProtected) {
    return NextResponse.next();
  }

  const token = request.cookies.get('session')?.value;

  if (!token) {
    const loginUrl = new URL('/login', request.url);
    loginUrl.searchParams.set('from', pathname);
    return NextResponse.redirect(loginUrl);
  }

  return NextResponse.next();
}

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

Middleware проверяет только наличие и базовую валидность токена - это первый рубеж. Полную проверку прав (роль, доступ к конкретному ресурсу) обычно оставляют на уровне API-маршрута или серверного компонента, потому что там доступна полноценная среда выполнения и можно безопасно обратиться к базе данных.

Как совместить геолокацию и авторизацию в одном middleware.ts

Middleware в проекте один, поэтому геолокационную и авторизационную логику приходится комбинировать в одной функции, а не писать отдельные файлы. Порядок проверок имеет значение: сначала быстрые проверки (совпадение пути, наличие cookie), затем более затратные (парсинг JWT).

Пример: геолокация + авторизация в одном middleware.ts
import { NextResponse } from 'next/server';
import { geolocation } from '@vercel/functions';
import type { NextRequest } from 'next/server';

const PROTECTED_PATHS = ['/dashboard', '/settings'];

export function middleware(request: NextRequest) {
  const { pathname } = request.nextUrl;

  const isProtected = PROTECTED_PATHS.some((path) => pathname.startsWith(path));
  if (isProtected) {
    const token = request.cookies.get('session')?.value;
    if (!token) {
      const loginUrl = new URL('/login', request.url);
      return NextResponse.redirect(loginUrl);
    }
    return NextResponse.next();
  }

  const { country } = geolocation(request);
  if (country === 'GB' && !pathname.startsWith('/uk')) {
    const url = request.nextUrl.clone();
    url.pathname = `/uk${pathname}`;
    return NextResponse.redirect(url);
  }

  return NextResponse.next();
}

export const config = {
  matcher: ['/dashboard/:path*', '/settings/:path*', '/((?!_next|api|favicon.ico).*)'],
};

Если логика разрастается больше чем на 2-3 сценария, стоит вынести проверки в отдельные функции внутри того же файла: Next.js допускает только один Middleware на приложение, но не ограничивает внутреннюю структуру кода.

Производительность: matcher и что нельзя делать в Middleware

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

Типичная ошибка - matcher по умолчанию '/:path*', который прогоняет через Middleware вообще все запросы, включая изображения и статику из _next/static. Правильный паттерн - явно исключить служебные пути через negative lookahead в регулярном выражении, как в примерах выше.

Второе правило - не делать в Middleware ничего, что блокирует ответ дольше нескольких миллисекунд. Запросы к внешним API, обращения к базе данных, тяжёлые вычисления - всё это увеличивает TTFB для каждого запроса, который проходит через matcher, а не только для одного эндпоинта. Middleware хорошо подходит для чтения cookie, заголовков и параметров URL и плохо подходит для любой сетевой операции с непредсказуемой задержкой.

Если нужна проверка, требующая обращения к внешнему сервису (например, к базе для валидации токена), стандартный паттерн - кешировать результат в самом JWT (claims) на момент выдачи токена, а не запрашивать его заново в Middleware при каждом запросе.

Хостинг определяет, как ведёт себя Middleware

Middleware в Next.js - это абстракция поверх конкретной edge-инфраструктуры, и это ощутимо влияет на то, где и как его стоит деплоить. На Vercel Middleware выполняется в их собственном Edge Runtime с полным набором геолокационных заголовков из коробки.

На Cloudflare Pages ситуация сложнее: Cloudflare сейчас рекомендует деплоить Next.js через OpenNext на Cloudflare Workers, а не через Cloudflare Pages напрямую, и часть возможностей - в частности, Node.js Middleware, появившийся в Next.js 15.2, - пока не поддерживается адаптером OpenNext для Cloudflare. Edge-рантайм Vercel не на 100% совпадает по API с Workers-рантаймом Cloudflare, поэтому Middleware, написанный и протестированный на Vercel, может потребовать доработки при переезде.

Перед тем как закладывать геолокацию или авторизацию в Middleware как часть архитектуры, стоит заранее сравнить хостинг для Next.js-проекта: смена платформы позже означает переписывание этой логики, а не просто передеплой.

Реальный кейс: что меняется на практике

В типовом проекте миграции лендинга с клиентской проверки авторизации на Middleware мы наблюдаем два измеримых эффекта. Во-первых, полностью исчезает visible flash защищённого контента - раньше он был заметен на медленных соединениях в течение 200-400 мс между первой отрисовкой и срабатыванием клиентского редиректа. Во-вторых, для геолокационных редиректов TTFB до целевой, уже локализованной страницы обычно ниже, чем при клиентском редиректе через useEffect, потому что редирект происходит до отправки HTML браузеру, а не после гидратации React.

Отдельный эффект от узкого matcher - снижение числа edge-инвокаций Middleware на 60-80% в типовых проектах, где раньше он по ошибке запускался и на статику: изображения, шрифты, файлы из _next/static.

Для кого актуально

Middleware с геолокацией и авторизацией имеет смысл для SaaS-продуктов с международной аудиторией, которым нужна региональная персонализация - цена, язык, юридические уведомления, и для B2B-сайтов с закрытыми разделами: клиентскими порталами, дашбордами, платным контентом. Особенно ощутим эффект для команд без выделенного backend-разработчика, которым нужно решить обе задачи средствами самого Next.js, не поднимая отдельный auth-сервис или reverse proxy.

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

Чем Middleware отличается от проверки авторизации в API-маршруте?

Middleware выполняется раньше - на уровне запроса, до рендеринга страницы и до вызова route handler. Это подходит для быстрой проверки, авторизован ли пользователь вообще, и редиректа. Полную проверку прав доступа к конкретному ресурсу - обращение к базе данных, роли, права - лучше делать в API-маршруте или серверном компоненте, где доступна полноценная среда выполнения.

Работает ли request.geo на любом хостинге?

Нет. Геолокационные заголовки вида x-vercel-ip-country появляются только при деплое на Vercel. На других платформах нужен свой источник данных - например, заголовок CF-IPCountry на Cloudflare или внешний IP-geolocation сервис. Код, завязанный только на заголовки Vercel, при смене хостинга перестаёт работать без явной ошибки, и это нужно проверять при каждой миграции.

Замедляет ли Middleware каждый запрос на сайте?

Замедляет, если matcher настроен широко и захватывает статику, изображения или служебные пути. При узком matcher, ограниченном конкретными маршрутами, и без сетевых запросов внутри функции, задержка обычно не превышает единиц миллисекунд - Middleware выполняется на edge, географически близко к пользователю.

Можно ли делать запрос к базе данных прямо в Middleware?

Технически - только если используется экспериментальный Node.js Middleware (с Next.js 15.2) и провайдер драйвера базы данных совместим с этой средой. В классическом Edge Runtime прямые TCP-соединения к базе недоступны. Стандартный паттерн - не запрашивать базу в Middleware вообще, а проверять подпись JWT, выпущенного с нужными claims заранее.

Что произойдёт, если забыть указать matcher?

Без явного matcher Middleware по умолчанию выполняется для всех путей, включая статические ресурсы из _next/static и _next/image. Это не ломает функциональность, но добавляет лишние edge-инвокации на каждый запрос статики - для высоконагруженного сайта это заметная и легко устранимая потеря производительности.

Итого

  • Middleware выполняется на edge до рендеринга - это единственное место, где геолокационный редирект и проверка авторизации происходят до того, как браузер получит HTML.
  • Геолокация зависит от хостинга: x-vercel-ip-country работает только на Vercel, для других платформ нужен свой источник данных.
  • Flash of protected content устраняется переносом проверки cookie или JWT в Middleware, а не в клиентский useEffect.
  • Узкий matcher и отсутствие сетевых операций внутри функции - главные рычаги производительности.

Если вы проектируете архитектуру сайта на Next.js и нужно совместить геолокацию, авторизацию и хостинг без переделки логики после запуска - опишите задачу команде Exceltic.dev. Разберём архитектуру и оценим объём работ.

Ещё статьи

Все →