Запропонувати правкуПокращити цю статтюДопрацюйте відповідь до «Проміжне програмне забезпечення в Next.js». Ваші зміни проходять модерацію перед публікацією.Потрібне підтвердженняКонтентЩо ви змінюєте🇺🇸EN🇺🇦UAПереглядЗаголовок (UA)Коротка відповідь (UA)**Middleware в Next.js** - це функція у файлі `middleware.ts`, яка запускається на Edge Runtime до того, як запити досягнуть маршрутів. Вона глобально обробляє auth, i18n та заголовки без змін у компонентах сторінок. ```ts export function middleware(request: NextRequest) { if (!request.cookies.get('session')) return NextResponse.redirect(new URL('/login', request.url)) return NextResponse.next() } export const config = { matcher: '/app/:path*' } ``` **Головне:** завжди додавай `config.matcher`, інакше middleware спрацьовує на кожен запит, включаючи статичні файли.Показується над повною відповіддю для швидкого нагадування.Відповідь (UA)Зображення**Middleware в Next.js** - це TypeScript-функція у файлі `middleware.ts`, яка запускається на Edge Network ще до того, як запит дійде до маршрутів. Вона дозволяє перехоплювати, перевіряти та перенаправляти запити без змін у компонентах сторінок. ## Теорія ### TL;DR - Аналогія: стійка безпеки в аеропорту. Кожен запит проходить через неї один раз, і ти вирішуєш - перенаправити, переписати URL або пропустити далі. - Запускається на Edge Runtime (V8-ізолятах, не на Node.js). Немає холодного старту, але й немає `fs` та Prisma. - Без `config.matcher` middleware спрацьовує на кожен запит, включаючи статичні файли. Завжди додавай matcher. - Підходить для: auth-перевірок, i18n, CORS-заголовків. Не підходить для: прямих запитів до бази даних. - Один файл `middleware.ts` на проект. Ланцюжок як у Express тут недоступний. ### Швидкий приклад ```ts // middleware.ts в кореневій директорії проекту import { NextResponse } from 'next/server' import type { NextRequest } from 'next/server' export function middleware(request: NextRequest) { if (request.nextUrl.pathname.startsWith('/dashboard')) { const token = request.cookies.get('auth')?.value if (!token) return NextResponse.redirect(new URL('/login', request.url)) } return NextResponse.next() } // Спрацьовує тільки на /dashboard та вкладених маршрутах export const config = { matcher: '/dashboard/:path*' } ``` Неавтентифікований запит до `/dashboard/settings` не знаходить cookie і потрапляє на `/login`. Автентифікований проходить далі без змін. ### Головна відмінність від Express Middleware в Next.js запускається на Edge Network до того, як запит досягне серверних функцій або сторінок. Express middleware запускається всередині процесу Node.js після старту сервера. На практиці: Next.js middleware розподілений глобально і не зберігає стан, тоді як Express може тримати стан і використовувати будь-який Node-модуль. Це серйозний компроміс, і на співбесіді його варто згадати. ### Коли використовувати - Перевірка авторизації на захищених маршрутах (`/app/*`, `/admin/*`) - middleware - Визначення локалі та переписування URL - middleware - Додавання заголовків безпеки (CSP, CORS) - middleware - Призначення bucket для A/B-тестів через cookies - middleware - Rate limiting `/api/*` через зовнішнє сховище типу Upstash - middleware - Складна логіка прав доступу, яка потребує бази даних - серверний компонент або route handler - Маршрути з понад 50% трафіку - варто подумати двічі, Edge має ліміти на запити ### Як це працює всередині Next.js збирає `middleware.ts` у WebAssembly-подібний модуль для Edge Runtime (V8-ізоляти). На кожен запит: matcher-регекс перевіряє шлях за O(1); якщо збігається, runtime викликає функцію з об'єктом `NextRequest` на основі Web Fetch API; функція повертає `NextResponse`, який сигналізує роутеру про редирект, переписування або продовження. Node.js у цьому процесі не задіяний. `NextRequest` надає доступ до `cookies`, `headers`, `nextUrl` та `geo`. Через `NextResponse` можна викликати `redirect()`, `rewrite()` або `next()` з опційними мутаціями заголовків. Напряму змінити об'єкт запиту не можна - він незмінний. ### Типові помилки **1. `export default` замість іменованого експорту.** ```ts // Неправильно - нічого не виконується, помилки збірки немає export default function middleware(request) { ... } // Правильно export function middleware(request: NextRequest) { ... } ``` Next.js вимагає іменованого експорту. Дефолтний компілюється нормально і мовчки не створює middleware під час виконання. **2. Відсутність `config.matcher`.** Без matcher middleware спрацьовує на кожен запит, включаючи `/_next/static`, `/_next/image` та `favicon.ico`. На безкоштовному тарифі Vercel це швидко вичерпує квоту. Додавай matcher. **3. `await fetch` без таймауту.** Edge Runtime має ліміт 30 секунд. Підвислий зовнішній fetch заблокує весь запит. Рішення: ```ts const controller = new AbortController() const timeout = setTimeout(() => controller.abort(), 5000) const res = await fetch('https://auth-service.example.com/verify', { signal: controller.signal }) clearTimeout(timeout) ``` **4. Використання Node.js-модулів.** `fs`, `crypto` (Node-версія), Prisma, `bcrypt` - жоден не працює в Edge Runtime. Збірка впаде з помилкою резолюції модуля. Використовуй Edge-сумісні альтернативи: `@vercel/kv`, `@upstash/redis`, Web Crypto API. **5. Пряма зміна об'єкта запиту.** `NextRequest` незмінний. Спроба встановити заголовки безпосередньо на ньому кине `TypeError`. Щоб передати дані вниз по стеку, використовуй заголовки відповіді, cookies або переписування URL із search params. ### Де зустрічається - **Vercel Dashboard**: middleware перенаправляє неавтентифікованих користувачів з `/team/*` - **Clerk / NextAuth.js**: перевірка session cookie на всіх захищених маршрутах - **next-intl**: переписує `/` на `/en` або `/de` на основі заголовка `Accept-Language` - **Інтеграції зі Stripe**: rate limiting `/api/stripe/*` через Upstash sliding window - **Create T3 App**: middleware для admin-авторизації та CSRF-заголовків ### Питання з інтерв'ю **Q:** Чим middleware відрізняється від route handlers у App Router? **A:** Middleware запускається до маршрутизації на Edge без React-контексту. Route handlers запускаються per-route на сервері з повним доступом до Node.js. Для auth: middleware - перший прохід перевірки, route handlers - бізнес-логіка. **Q:** Чи може middleware отримати доступ до бази даних напряму? **A:** Ні. Edge Runtime не має постійних підключень. Для стану використовуй зовнішні сервіси: Upstash Redis, PlanetScale serverless driver або Vercel KV. Middleware перевіряє токен, серверні компоненти виконують запит на права доступу. **Q:** Як поєднати кілька middleware? **A:** На проект є один `middleware.ts`. Compose логіку послідовними `if`-блоками або використовуй бібліотеку на кшталт `hono` для міні-роутів всередині одного файлу. Ланцюжка як у Express `app.use()` тут немає. **Q:** Який синтаксис matcher для пропуску статичних файлів? **A:** `'/((?!api|_next/static|_next/image|favicon.ico).*)'`. Негативний lookahead пропускає ці префікси. Без нього middleware спрацьовує на кожен статичний ресурс. **Q:** Що відбувається, якщо middleware має одночасно зробити редирект і встановити cookie? **A:** Виграє перший повернутий `NextResponse`. Після `return NextResponse.redirect(...)` функція завершується. Якщо потрібно встановити cookie на редиректі, створи відповідь редиректу, виклич на ній `.cookies.set()`, потім поверни її. **Q:** Що змінилося в Next.js 15 для middleware? **A:** Додана підтримка Turbopack, async-конфігурація matcher тепер доступна в експериментальному режимі. Основний API (`NextRequest`, `NextResponse`, `config.matcher`) залишився без змін. ## Приклади ### Auth-перевірка зі збереженням URL ```ts import { NextResponse } from 'next/server' import type { NextRequest } from 'next/server' export function middleware(request: NextRequest) { const { pathname } = request.nextUrl const token = request.cookies.get('session')?.value if (pathname.startsWith('/app') && !token) { // Зберігаємо початковий URL для редиректу після логіну const loginUrl = new URL('/login', request.url) loginUrl.searchParams.set('from', pathname) return NextResponse.redirect(loginUrl) } return NextResponse.next() } export const config = { matcher: '/app/:path*' } // /app/settings без cookie → /login?from=/app/settings // /app/settings з дійсним cookie → проходить далі ``` Після логіну сторінка входу читає `?from` і повертає користувача туди, де він був. Маленька деталь, яка робить auth-потік відчутно зручнішим у продакшені. ### Визначення локалі та переписування URL ```ts import { NextResponse } from 'next/server' import type { NextRequest } from 'next/server' const locales = ['en', 'de', 'fr'] export function middleware(request: NextRequest) { const { pathname } = request.nextUrl const hasLocale = locales.some( locale => pathname.startsWith(`/${locale}/`) || pathname === `/${locale}` ) if (hasLocale) return NextResponse.next() const acceptLanguage = request.headers.get('accept-language') ?? '' const preferred = acceptLanguage.split(',')[0].slice(0, 2) const locale = locales.includes(preferred) ? preferred : 'en' // rewrite - URL в браузері не змінюється return NextResponse.rewrite(new URL(`/${locale}${pathname}`, request.url)) } export const config = { matcher: '/((?!api|_next/static|_next/image|favicon.ico).*)' } // GET / з Accept-Language: de,en → рендерить /de/ без зміни URL в браузері ``` Різниця між `redirect` і `rewrite` тут важлива. `rewrite` змінює що рендериться, `redirect` змінює URL, який бачить користувач. Для i18n зазвичай потрібен саме `rewrite`. ### Rate limiting для API через Upstash Redis ```ts import { NextResponse } from 'next/server' import type { NextRequest } from 'next/server' import { Ratelimit } from '@upstash/ratelimit' import { Redis } from '@upstash/redis' const ratelimit = new Ratelimit({ redis: Redis.fromEnv(), limiter: Ratelimit.slidingWindow(10, '1 m'), // 10 запитів на хвилину з одного IP }) export async function middleware(request: NextRequest) { if (request.nextUrl.pathname.startsWith('/api')) { 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, headers: { 'Retry-After': '60' } }) } const response = NextResponse.next() response.headers.set('Access-Control-Allow-Origin', '*') response.headers.set('Access-Control-Allow-Methods', 'GET,POST,PUT,DELETE') return response } return NextResponse.next() } export const config = { matcher: '/api/:path*' } // Кожен IP отримує 10 запитів на хвилину для /api-маршрутів // Перевищення → 429 із заголовком Retry-After ``` Важливий момент: якщо `UPSTASH_REDIS_REST_URL` та `UPSTASH_REDIS_REST_TOKEN` відсутні в environment, `Redis.fromEnv()` кидає помилку при старті і вбиває весь middleware-шар. Додай ці змінні до деплою.Для рев’юераПримітка для модератора (необов’язково)Бачить лише модератор. Прискорює рев’ю.