Next.js 16: міграція з middleware.ts на proxy.ts — повний гід

У Next.js 16 файл middleware.ts офіційно перейменовано на proxy.ts. Розбираємо причини, codemod-міграцію, нові обмеження runtime та безпечні патерни автентифікації, які витримають CVE-2025-29927.

Next.js 16 proxy.ts: Migrate from middleware

Реліз Next.js 16 приніс одну з найпомітніших змін у роутингу за останні кілька років: файл middleware.ts офіційно перейменовано на proxy.ts. На перший погляд — звичайна косметика. Але якщо чесно, за нею стоїть зміна філософії: команда Vercel хоче, щоб ви менше покладалися на цей рівень для бізнес-логіки та автентифікації, а більше — на Server Components, Server Actions і Data Access Layer (DAL).

У цьому посібнику розберемо: навіщо зробили перейменування, як виконати міграцію буквально за хвилину завдяки codemod, які пастки чекають на матчерах і чому уразливість CVE-2025-29927 назавжди змінила підхід до автентифікації в App Router.

Чесно зізнаюся: коли я вперше побачив changelog, реакція була «ну от, знову». Але після кількох вечорів з новим API стало зрозуміло — зміна назрівала давно.

Що саме змінилося в Next.js 16

До 16-ї версії точкою входу для перехоплення запитів був middleware.ts у корені проєкту. Тепер той самий файл називається proxy.ts, а замість іменованого експорту middleware очікується proxy (або default-експорт). Логіка не змінилася — інтерфейс той самий, набір API той самий. Просто сигнатура файлу нова.

Ключові факти, які варто запам'ятати:

  • Файл: middleware.tsproxy.ts у корені проєкту або в src/.
  • Експорт: функція middlewareproxy.
  • Конфіг-флаги: skipMiddlewareUrlNormalizeskipProxyUrlNormalize, аналогічно для skipTrailingSlashRedirect.
  • Runtime: proxy.ts працює на Node.js runtime за замовчуванням і Edge не підтримує.
  • Сумісність: middleware.ts досі працює, але задепрековано — на Edge ще можна, на Node — вже ні.

Навіщо було перейменовувати

В офіційному блозі команда Next.js пояснила доволі просто: слово «middleware» історично перенасичене значеннями. У світі Express це проміжний шар обробки запитів, у Redux — мідлвара екшенів, у NestJS — guards/interceptors. Програмісти автоматично припускали, що Next.js middleware — це місце для бізнес-логіки, повноцінної автентифікації, читання БД. А насправді він задумувався як тонкий мережевий шар перед застосунком: редіректи, A/B-тестування, geo-routing, базові перевірки заголовків.

Назва proxy чесніше описує суть: це reverse-proxy перед вашим додатком. Він має знаходитися ближче до клієнта, бути швидким, без БД-викликів і без важких операцій. Ось і все.

Покрокова міграція: codemod за 30 секунд

Vercel надає офіційний codemod, який автоматично перейменує файл і експорт:

npx @next/codemod@latest middleware-to-proxy ./

Що зробить codemod:

  1. Перейменує middleware.ts на proxy.ts (або .js).
  2. Замінить export middleware на proxy.
  3. Оновить імпорти, які посилаються на стару назву.
  4. Замінить конфіг-флаги в next.config.js.

Після запуску обов'язково перевірте git diff — codemod не зачіпає вашу бізнес-логіку, лише перейменування. Якщо ви хочете залишитися на Edge runtime (наприклад, через geo-routing з низькою латентністю), не запускайте codemod і продовжуйте використовувати middleware.ts. Тільки майте на увазі: це шлях у нікуди.

Базовий proxy.ts: мінімальний приклад

Ось скелет нового proxy.ts, який перевіряє наявність session-кукі та редіректить неавторизованих користувачів:

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

export function proxy(request: NextRequest) {
  const session = request.cookies.get('session')?.value;
  const { pathname } = request.nextUrl;

  const isProtected = pathname.startsWith('/dashboard') ||
                      pathname.startsWith('/settings');

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

  return NextResponse.next();
}

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

Зверніть увагу: ми не валідуємо JWT, не звертаємося до бази, не розшифровуємо токен. Ми лише перевіряємо наявність кукі — це швидка перевірка на рівні мережі. Сама автентифікація відбувається пізніше, ближче до даних.

Урок CVE-2025-29927: proxy — не межа безпеки

У березні 2025 року дослідники розкрили уразливість CVE-2025-29927 з оцінкою CVSS 9.1. Атакуючий міг повністю обійти middleware-перевірки, додавши заголовок x-middleware-subrequest до запиту. Уразливі версії: 11.1.4–12.3.4, 13.0.0–13.5.8, 14.0.0–14.2.24, 15.0.0–15.2.2.

Уроком стало архітектурне правило, яке тепер навіть документація Next.js повторює явно:

Proxy / middleware — це не межа безпеки. Це маршрутизатор. Реальні перевірки автентифікації мають жити у Route Handlers, Server Actions і Data Access Layer.

Це означає, що навіть якщо ваш proxy.ts ідеально перевіряє кукі, кожен Server Action і кожен запит до БД має повторно перевіряти сесію. Звучить як дублювання? Так. Але саме воно врятує вас від наступного CVE, помилки в матчері або випадкового рефакторингу, який «забере» певний маршрут з-під захисту.

Data Access Layer: золотий стандарт 2026

DAL — це централізований модуль, через який проходять усі читання й записи даних. У ньому, перед поверненням даних, ви завжди викликаєте verifySession(). Виглядає це приблизно так:

// lib/dal.ts
import 'server-only';
import { cookies } from 'next/headers';
import { cache } from 'react';
import { redirect } from 'next/navigation';
import { decrypt } from './session';
import { db } from './db';

export const verifySession = cache(async () => {
  const cookie = (await cookies()).get('session')?.value;
  const session = await decrypt(cookie);

  if (!session?.userId) {
    redirect('/login');
  }

  return { isAuth: true, userId: session.userId };
});

export const getCurrentUser = cache(async () => {
  const session = await verifySession();

  const user = await db.user.findUnique({
    where: { id: session.userId },
    select: { id: true, email: true, role: true },
  });

  return user;
});

Тепер у будь-якому Server Component або Server Action достатньо викликати getCurrentUser() — і автентифікація автоматично перевіряється. Завдяки cache() від React один запит = одна перевірка, навіть якщо викликати функцію десять разів у різних компонентах. Дуже зручно.

HttpOnly-кукі замість localStorage

Ще одне залізне правило: токен сесії має жити в HttpOnly-кукі, а не в localStorage. Причини банальні, але часто забуваються:

  • localStorage невидимий серверу. Ваш proxy.ts просто не зможе його прочитати — а отже, перевірка автентифікації на рівні мережі неможлива.
  • XSS читає localStorage без проблем. Будь-який JS на сторінці отримує доступ до сховища. HttpOnly-флаг закриває кукі від клієнтського JS.
  • HttpOnly-кукі автоматично надсилаються з кожним запитом, тому сервер може перевірити сесію без зайвих заголовків.
// app/api/login/route.ts
import { cookies } from 'next/headers';

export async function POST(req: Request) {
  // ... перевірка credentials, генерація токена
  const cookieStore = await cookies();
  cookieStore.set('session', token, {
    httpOnly: true,
    secure: process.env.NODE_ENV === 'production',
    sameSite: 'lax',
    maxAge: 60 * 60 * 24, // 24 години
    path: '/',
  });

  return Response.json({ ok: true });
}

Matcher-патерни: типові пастки

Найпоширеніша помилка — занадто загальний матчер, який ловить власні редіректи й створює нескінченний цикл (ERR_TOO_MANY_REDIRECTS). Я сам колись на цьому згорів — годину дебажив, поки не зрозумів, що матчер ловить сам себе. Класичний антипатерн:

// ❌ Цей патерн ловить /api/auth/signin і робить нескінченний редірект
export const config = {
  matcher: ['/((?!api|_next/static|_next/image|.*\.png$).*)'],
};

Краще описати маршрути явно:

// ✅ Явний список захищених сегментів
export const config = {
  matcher: [
    '/dashboard/:path*',
    '/admin/:path*',
    '/settings/:path*',
  ],
};

Якщо все-таки потрібен «catch-all», обов'язково перевіряйте всередині proxy, чи ви вже не на сторінці входу:

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

  if (pathname.startsWith('/login') || pathname.startsWith('/api/auth')) {
    return NextResponse.next();
  }
  // ... решта логіки
}

Rate-limiting на рівні proxy

Хоч важка логіка не належить proxy, обмеження частоти запитів — навпаки, ідеальний кейс. Це швидка перевірка, яка має відсіювати трафік ДО того, як він дійде до Server Actions. Приклад на Upstash Redis:

// proxy.ts
import { Ratelimit } from '@upstash/ratelimit';
import { Redis } from '@upstash/redis';

const ratelimit = new Ratelimit({
  redis: Redis.fromEnv(),
  limiter: Ratelimit.slidingWindow(10, '10 s'),
});

export async function proxy(request: NextRequest) {
  if (request.nextUrl.pathname.startsWith('/api/login')) {
    const ip = request.headers.get('x-forwarded-for') ?? 'anonymous';
    const { success } = await ratelimit.limit(ip);

    if (!success) {
      return new NextResponse('Too many requests', { status: 429 });
    }
  }

  return NextResponse.next();
}

RBAC та перевірки ролей

Через Partial Rendering у Next.js 16 layout-и не перерендеряться при кожному переході — тому перевірки ролей у layout-ах ненадійні. Виносьте їх у Server Components сторінок або, ще краще, у DAL-функції, які повертають дані. Приклад:

// app/admin/page.tsx
import { getCurrentUser } from '@/lib/dal';
import { redirect } from 'next/navigation';

export default async function AdminPage() {
  const user = await getCurrentUser();

  if (user.role !== 'admin') {
    redirect('/forbidden');
  }

  return <AdminDashboard user={user} />;
}

Чек-лист міграції з middleware.ts на proxy.ts

  • ✅ Запустіть npx @next/codemod@latest middleware-to-proxy ./
  • ✅ Перевірте git diff: перейменування файлу та експорту
  • ✅ Оновіть конфіг-флаги (skipProxyUrlNormalize тощо)
  • ✅ Винесіть автентифікацію в DAL з verifySession()
  • ✅ Перенесіть session-токени в HttpOnly + Secure + SameSite cookies
  • ✅ Замініть generic-матчер на явний список маршрутів
  • ✅ Додайте rate-limiting для /api/login та /api/auth
  • ✅ Перевірте, що в Server Actions є власна перевірка сесії
  • ✅ Оновіть Next.js до версії, що містить фікс CVE-2025-29927 (15.2.3+ або 16+)

FAQ

Чи можна продовжувати використовувати middleware.ts у Next.js 16?

Так, але з обмеженнями. Файл middleware.ts досі працює на Edge runtime, проте офіційно задепрековано і буде видалено в одній з наступних версій. На Node.js runtime він уже не підтримується. Рекомендується перейти на proxy.ts якомога раніше — поки міграція простіша, ніж зворотний бій з регресіями.

Чи працює proxy.ts на Edge Runtime?

Ні. proxy.ts запускається на Node.js runtime, і це не налаштовується. Якщо вам критично потрібен Edge (наприклад, для geo-routing з мінімальною латентністю), залишайтесь на middleware.ts доти, доки команда Vercel не запропонує заміну.

Чи безпечно покладатися лише на proxy.ts для автентифікації?

Ні. Уразливість CVE-2025-29927 показала, що навіть бездоганний proxy може бути обійдений через помилку фреймворка. Реальні перевірки сесії мають дублюватися в Server Actions, Route Handlers і Data Access Layer. Proxy — це маршрутизатор, а не межа безпеки.

Як перевіряти JWT у proxy.ts?

Не варто — JWT-валідація з криптографією є важкою для мережевого шару. У proxy перевіряйте лише наявність кукі. Повну валідацію з перевіркою підпису, expires і ролей виконуйте в DAL, який кешується через cache() і викликається в Server Components.

Чи потрібно переписувати Auth.js / NextAuth під proxy.ts?

Codemod автоматично перейменує файл, але перевірте офіційну документацію вашої auth-бібліотеки. Auth.js v5 та Better Auth v1 уже випустили оновлення з підтримкою proxy.ts. Auth0 SDK теж адаптовано — у документації описано приклад інтеграції через auth0.middleware(request) у новому файлі proxy.ts.

Підсумок

Перейменування middleware.ts на proxy.ts — не косметика, а явний сигнал: цей рівень не для бізнес-логіки. Тримайте його тонким, передбачуваним і швидким. Автентифікацію виносьте в DAL, токени — в HttpOnly-кукі, перевірки — повторюйте в кожному Server Action. Тоді наступний CVE для вас буде новиною з блогу, а не інцидентом продакшну.

Про Автора Editorial Team

Our team of expert writers and editors.