Реліз 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.ts→proxy.tsу корені проєкту або вsrc/. - Експорт: функція
middleware→proxy. - Конфіг-флаги:
skipMiddlewareUrlNormalize→skipProxyUrlNormalize, аналогічно для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:
- Перейменує
middleware.tsнаproxy.ts(або.js). - Замінить export
middlewareнаproxy. - Оновить імпорти, які посилаються на стару назву.
- Замінить конфіг-флаги в
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 для вас буде новиною з блогу, а не інцидентом продакшну.