Middleware и matcher
Middleware выполняется до рендера страницы. Подходит для авторизации, редиректов, A/B, геолокации.
Создание
middleware.ts в корне (рядом с app/):
import { NextResponse } from 'next/server';
import type { NextRequest } from 'next/server';
export function middleware(request: NextRequest) {
return NextResponse.next();
}
По умолчанию применяется ко всем роутам.
Matcher
Ограничить:
export const config = {
matcher: ['/dashboard/:path*', '/admin/:path*'],
};
:path* — любая вложенность.
Исключить:
export const config = {
matcher: ['/((?!api|_next/static|_next/image|favicon.ico).*)'],
};
Redirect
export function middleware(request: NextRequest) {
if (!request.cookies.has('token')) {
return NextResponse.redirect(new URL('/login', request.url));
}
return NextResponse.next();
}
Rewrite
Меняет URL без редиректа:
return NextResponse.rewrite(new URL('/dashboard', request.url));
Заголовки
const response = NextResponse.next();
response.headers.set('X-Custom', 'value');
return response;
Cookies
const token = request.cookies.get('token')?.value;
const response = NextResponse.next();
response.cookies.set('visited', 'true', {
httpOnly: true,
secure: true,
sameSite: 'lax',
});
return response;
Авторизация
import { verifyToken } from '@/lib/auth';
export async function middleware(request: NextRequest) {
const token = request.cookies.get('token')?.value;
if (!token) {
const loginUrl = new URL('/login', request.url);
loginUrl.searchParams.set('from', request.nextUrl.pathname);
return NextResponse.redirect(loginUrl);
}
const user = await verifyToken(token);
if (!user) {
return NextResponse.redirect(new URL('/login', request.url));
}
const response = NextResponse.next();
response.headers.set('X-User-Id', String(user.id));
return response;
}
export const config = {
matcher: ['/dashboard/:path*', '/admin/:path*'],
};
Роли
export function middleware(request: NextRequest) {
const role = request.cookies.get('role')?.value;
if (request.nextUrl.pathname.startsWith('/admin') && role !== 'admin') {
return NextResponse.redirect(new URL('/forbidden', request.url));
}
return NextResponse.next();
}
A/B-тесты
export function middleware(request: NextRequest) {
const bucket = request.cookies.get('bucket')?.value ?? (Math.random() < 0.5 ? 'a' : 'b');
const response = NextResponse.rewrite(new URL(`/${bucket}${request.nextUrl.pathname}`, request.url));
response.cookies.set('bucket', bucket);
return response;
}
Геолокация
export function middleware(request: NextRequest) {
const country = request.geo?.country ?? 'US';
if (country === 'RU' && !request.nextUrl.pathname.startsWith('/ru')) {
return NextResponse.redirect(new URL(`/ru${request.nextUrl.pathname}`, request.url));
}
return NextResponse.next();
}
geo доступен на Vercel. В других средах — через заголовки.
Ограничения
Middleware работает в Edge Runtime:
- Нет Node.js API
- Ограниченный размер
- Быстрый запуск
Нельзя:
- Работать с БД напрямую
- Использовать тяжёлые библиотеки
Для БД — вызывай API или используй fetch.
Middleware не защищает API
Middleware защищает страницы, но API-роуты и Server Actions нужно проверять отдельно:
'use server';
export async function deletePost(id: number) {
const session = await auth();
if (!session) throw new Error('Unauthorized');
// ...
}
Matcher с параметрами
export const config = {
matcher: [
'/dashboard/:path*',
'/posts/:id',
'/api/:path*',
],
};
Итоги
middleware.tsв корнеmatcherдля ограниченияredirect,rewrite,next- Cookies и заголовки
- Edge Runtime — ограничения
- Не защищает API — проверяй отдельно