Next.js middleware runs before requests are completed. Here's how to use it for authentication, redirects, and more.
Basic Middleware#
1// middleware.ts (in project root)
2import { NextResponse } from 'next/server';
3import type { NextRequest } from 'next/server';
4
5export function middleware(request: NextRequest) {
6 // Log all requests
7 console.log(`${request.method} ${request.url}`);
8
9 // Continue with the request
10 return NextResponse.next();
11}
12
13// Configure which paths middleware runs on
14export const config = {
15 matcher: [
16 // Match all paths except static files
17 '/((?!_next/static|_next/image|favicon.ico).*)',
18 ],
19};Authentication Middleware#
1// middleware.ts
2import { NextResponse } from 'next/server';
3import type { NextRequest } from 'next/server';
4import { verifyToken } from './lib/auth';
5
6// Protected routes
7const protectedPaths = ['/dashboard', '/settings', '/api/protected'];
8const authPaths = ['/login', '/register'];
9
10export async function middleware(request: NextRequest) {
11 const { pathname } = request.nextUrl;
12 const token = request.cookies.get('auth-token')?.value;
13
14 // Check if path is protected
15 const isProtected = protectedPaths.some((path) =>
16 pathname.startsWith(path)
17 );
18
19 const isAuthPath = authPaths.some((path) =>
20 pathname.startsWith(path)
21 );
22
23 // Verify token
24 const session = token ? await verifyToken(token) : null;
25
26 // Redirect unauthenticated users from protected routes
27 if (isProtected && !session) {
28 const url = request.nextUrl.clone();
29 url.pathname = '/login';
30 url.searchParams.set('callbackUrl', pathname);
31 return NextResponse.redirect(url);
32 }
33
34 // Redirect authenticated users from auth pages
35 if (isAuthPath && session) {
36 const callbackUrl = request.nextUrl.searchParams.get('callbackUrl');
37 return NextResponse.redirect(
38 new URL(callbackUrl || '/dashboard', request.url)
39 );
40 }
41
42 // Add user info to headers for server components
43 const response = NextResponse.next();
44 if (session) {
45 response.headers.set('x-user-id', session.userId);
46 }
47
48 return response;
49}
50
51export const config = {
52 matcher: ['/dashboard/:path*', '/settings/:path*', '/login', '/register'],
53};Role-Based Access Control#
1// middleware.ts
2import { NextResponse } from 'next/server';
3import type { NextRequest } from 'next/server';
4
5interface RouteConfig {
6 path: string;
7 roles: string[];
8}
9
10const routeConfigs: RouteConfig[] = [
11 { path: '/admin', roles: ['admin'] },
12 { path: '/dashboard', roles: ['admin', 'user'] },
13 { path: '/api/admin', roles: ['admin'] },
14];
15
16export async function middleware(request: NextRequest) {
17 const { pathname } = request.nextUrl;
18
19 // Find matching route config
20 const config = routeConfigs.find((route) =>
21 pathname.startsWith(route.path)
22 );
23
24 if (!config) {
25 return NextResponse.next();
26 }
27
28 // Get user session
29 const token = request.cookies.get('auth-token')?.value;
30 const session = token ? await verifySession(token) : null;
31
32 if (!session) {
33 return NextResponse.redirect(new URL('/login', request.url));
34 }
35
36 // Check role
37 if (!config.roles.includes(session.role)) {
38 return NextResponse.redirect(new URL('/unauthorized', request.url));
39 }
40
41 return NextResponse.next();
42}Geolocation and Localization#
1// middleware.ts
2import { NextResponse } from 'next/server';
3import type { NextRequest } from 'next/server';
4
5const SUPPORTED_LOCALES = ['en', 'es', 'fr', 'de'];
6const DEFAULT_LOCALE = 'en';
7
8export function middleware(request: NextRequest) {
9 const { pathname } = request.nextUrl;
10
11 // Skip if already has locale prefix
12 if (SUPPORTED_LOCALES.some((locale) => pathname.startsWith(`/${locale}`))) {
13 return NextResponse.next();
14 }
15
16 // Get locale from various sources
17 const locale = getPreferredLocale(request);
18
19 // Redirect to localized path
20 const url = request.nextUrl.clone();
21 url.pathname = `/${locale}${pathname}`;
22
23 return NextResponse.redirect(url);
24}
25
26function getPreferredLocale(request: NextRequest): string {
27 // 1. Check cookie preference
28 const cookieLocale = request.cookies.get('locale')?.value;
29 if (cookieLocale && SUPPORTED_LOCALES.includes(cookieLocale)) {
30 return cookieLocale;
31 }
32
33 // 2. Check Accept-Language header
34 const acceptLanguage = request.headers.get('accept-language');
35 if (acceptLanguage) {
36 const browserLocale = acceptLanguage
37 .split(',')[0]
38 .split('-')[0]
39 .toLowerCase();
40
41 if (SUPPORTED_LOCALES.includes(browserLocale)) {
42 return browserLocale;
43 }
44 }
45
46 // 3. Check geo location (Vercel)
47 const country = request.geo?.country;
48 if (country) {
49 const countryLocale = COUNTRY_LOCALE_MAP[country];
50 if (countryLocale && SUPPORTED_LOCALES.includes(countryLocale)) {
51 return countryLocale;
52 }
53 }
54
55 return DEFAULT_LOCALE;
56}
57
58export const config = {
59 matcher: ['/((?!api|_next|.*\\..*).*)'],
60};A/B Testing#
1// middleware.ts
2import { NextResponse } from 'next/server';
3import type { NextRequest } from 'next/server';
4
5const EXPERIMENTS = {
6 'new-homepage': {
7 variants: ['control', 'variant-a', 'variant-b'],
8 weights: [0.34, 0.33, 0.33],
9 },
10 'checkout-flow': {
11 variants: ['control', 'simplified'],
12 weights: [0.5, 0.5],
13 },
14};
15
16export function middleware(request: NextRequest) {
17 const response = NextResponse.next();
18
19 // Assign experiments
20 for (const [name, config] of Object.entries(EXPERIMENTS)) {
21 const cookieName = `experiment-${name}`;
22 let variant = request.cookies.get(cookieName)?.value;
23
24 // Assign new variant if not set
25 if (!variant || !config.variants.includes(variant)) {
26 variant = selectVariant(config.variants, config.weights);
27 response.cookies.set(cookieName, variant, {
28 maxAge: 60 * 60 * 24 * 30, // 30 days
29 });
30 }
31
32 // Add to headers for server components
33 response.headers.set(`x-${cookieName}`, variant);
34 }
35
36 return response;
37}
38
39function selectVariant(variants: string[], weights: number[]): string {
40 const random = Math.random();
41 let cumulative = 0;
42
43 for (let i = 0; i < variants.length; i++) {
44 cumulative += weights[i];
45 if (random < cumulative) {
46 return variants[i];
47 }
48 }
49
50 return variants[variants.length - 1];
51}Rate Limiting#
1// middleware.ts
2import { NextResponse } from 'next/server';
3import type { NextRequest } from 'next/server';
4
5// Simple in-memory rate limiting (use Redis in production)
6const rateLimit = new Map<string, { count: number; timestamp: number }>();
7
8const RATE_LIMIT = 100; // requests
9const WINDOW_MS = 60 * 1000; // 1 minute
10
11export function middleware(request: NextRequest) {
12 const ip = request.ip ?? request.headers.get('x-forwarded-for') ?? 'unknown';
13 const key = `rate-limit:${ip}`;
14
15 const now = Date.now();
16 const windowStart = now - WINDOW_MS;
17
18 // Get or create rate limit entry
19 let entry = rateLimit.get(key);
20
21 if (!entry || entry.timestamp < windowStart) {
22 entry = { count: 0, timestamp: now };
23 }
24
25 entry.count++;
26 rateLimit.set(key, entry);
27
28 // Check if rate limited
29 if (entry.count > RATE_LIMIT) {
30 return new NextResponse('Too Many Requests', {
31 status: 429,
32 headers: {
33 'Retry-After': String(Math.ceil((entry.timestamp + WINDOW_MS - now) / 1000)),
34 },
35 });
36 }
37
38 // Add rate limit headers
39 const response = NextResponse.next();
40 response.headers.set('X-RateLimit-Limit', String(RATE_LIMIT));
41 response.headers.set('X-RateLimit-Remaining', String(RATE_LIMIT - entry.count));
42
43 return response;
44}
45
46export const config = {
47 matcher: '/api/:path*',
48};Request/Response Modification#
1// middleware.ts
2import { NextResponse } from 'next/server';
3import type { NextRequest } from 'next/server';
4
5export function middleware(request: NextRequest) {
6 // Clone and modify request headers
7 const requestHeaders = new Headers(request.headers);
8 requestHeaders.set('x-request-id', crypto.randomUUID());
9 requestHeaders.set('x-custom-header', 'middleware-value');
10
11 // Rewrite URL (internal redirect)
12 if (request.nextUrl.pathname === '/old-path') {
13 return NextResponse.rewrite(new URL('/new-path', request.url));
14 }
15
16 // Modify response
17 const response = NextResponse.next({
18 request: {
19 headers: requestHeaders,
20 },
21 });
22
23 // Add security headers
24 response.headers.set('X-Frame-Options', 'DENY');
25 response.headers.set('X-Content-Type-Options', 'nosniff');
26 response.headers.set('Referrer-Policy', 'strict-origin-when-cross-origin');
27
28 // Set CORS headers for API routes
29 if (request.nextUrl.pathname.startsWith('/api/')) {
30 response.headers.set('Access-Control-Allow-Origin', '*');
31 response.headers.set('Access-Control-Allow-Methods', 'GET, POST, PUT, DELETE');
32 }
33
34 return response;
35}Combining Multiple Middleware#
1// middleware.ts
2import { NextResponse } from 'next/server';
3import type { NextRequest } from 'next/server';
4
5type MiddlewareFunction = (
6 request: NextRequest,
7 response: NextResponse
8) => NextResponse | Promise<NextResponse>;
9
10const middlewares: MiddlewareFunction[] = [
11 authMiddleware,
12 localeMiddleware,
13 rateLimitMiddleware,
14 securityHeadersMiddleware,
15];
16
17export async function middleware(request: NextRequest) {
18 let response = NextResponse.next();
19
20 for (const mw of middlewares) {
21 response = await mw(request, response);
22
23 // Stop chain if response is not NextResponse.next()
24 if (response.status !== 200 || response.headers.get('x-middleware-rewrite')) {
25 break;
26 }
27 }
28
29 return response;
30}
31
32// Individual middleware functions
33function authMiddleware(request: NextRequest, response: NextResponse) {
34 // Auth logic
35 return response;
36}
37
38function localeMiddleware(request: NextRequest, response: NextResponse) {
39 // Locale logic
40 return response;
41}Best Practices#
Performance:
✓ Keep middleware fast (runs on every request)
✓ Avoid heavy computations
✓ Use Edge runtime efficiently
✓ Cache when possible
Security:
✓ Validate tokens properly
✓ Sanitize inputs
✓ Set security headers
✓ Handle errors gracefully
Configuration:
✓ Use specific matchers
✓ Exclude static files
✓ Order middleware logically
✓ Test all paths
Conclusion#
Next.js middleware enables powerful request-time logic including authentication, localization, A/B testing, and rate limiting. Keep middleware fast since it runs on every matched request. Use specific matchers to avoid unnecessary execution and combine multiple concerns thoughtfully.