Back to Blog
Next.jsMiddlewareAuthenticationEdge

Next.js Middleware Patterns

Master Next.js middleware for authentication, redirects, and request modification. From basic usage to advanced patterns.

B
Bootspring Team
Engineering
February 8, 2022
6 min read

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.

Share this article

Help spread the word about Bootspring