Skip to main content

Проміжне програмне забезпечення в Next.js

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-шар. Додай ці змінні до деплою.

Коротка відповідь

Для співбесіди
Premium

Коротка відповідь допоможе вам впевнено відповідати на цю тему під час співбесіди.

Дочитали статтю?