Middleware Patterns
Middleware runs before every request, allowing you to modify responses, redirect, rewrite, or add headers. It's powerful for cross-cutting concerns.
Basic Middleware
Create middleware.ts in your project root:
// middleware.ts
import { NextResponse } from 'next/server'
import type { NextRequest } from 'next/server'
export function middleware(request: NextRequest) {
// Runs on every matched request
console.log('Request to:', request.nextUrl.pathname)
return NextResponse.next()
}
// Configure which routes to run on
export const config = {
matcher: [
// Match all except static files
'/((?!_next/static|_next/image|favicon.ico).*)',
],
}
Route Matching
Control which routes middleware runs on:
export const config = {
matcher: [
// Single path
'/dashboard',
// All paths under /api
'/api/:path*',
// Multiple specific paths
'/about',
'/contact',
// Regex pattern
'/((?!api|_next/static|favicon.ico).*)',
],
}
Or match conditionally in the function:
export function middleware(request: NextRequest) {
if (request.nextUrl.pathname.startsWith('/admin')) {
// Admin logic
}
if (request.nextUrl.pathname.startsWith('/api')) {
// API logic
}
return NextResponse.next()
}
Common Patterns
Authentication Check
import { NextResponse } from 'next/server'
import type { NextRequest } from 'next/server'
export function middleware(request: NextRequest) {
const token = request.cookies.get('session')?.value
// Protected routes
if (request.nextUrl.pathname.startsWith('/dashboard')) {
if (!token) {
const loginUrl = new URL('/login', request.url)
loginUrl.searchParams.set('from', request.nextUrl.pathname)
return NextResponse.redirect(loginUrl)
}
}
// Redirect logged-in users away from auth pages
if (request.nextUrl.pathname.startsWith('/login') && token) {
return NextResponse.redirect(new URL('/dashboard', request.url))
}
return NextResponse.next()
}
export const config = {
matcher: ['/dashboard/:path*', '/login', '/signup'],
}
Geolocation-Based Redirect
export function middleware(request: NextRequest) {
const country = request.geo?.country || 'US'
if (country === 'GB' && !request.nextUrl.pathname.startsWith('/uk')) {
return NextResponse.redirect(new URL('/uk' + request.nextUrl.pathname, request.url))
}
return NextResponse.next()
}
Adding Headers
export function middleware(request: NextRequest) {
const response = NextResponse.next()
// Add security headers
response.headers.set('X-Frame-Options', 'DENY')
response.headers.set('X-Content-Type-Options', 'nosniff')
// Add request ID for tracing
response.headers.set('X-Request-Id', crypto.randomUUID())
return response
}
URL Rewriting
Serve different content without changing the URL:
export function middleware(request: NextRequest) {
const hostname = request.headers.get('host') || ''
// Multi-tenant: Rewrite subdomain to path
if (hostname.startsWith('blog.')) {
return NextResponse.rewrite(
new URL('/blog' + request.nextUrl.pathname, request.url)
)
}
// A/B testing
const bucket = request.cookies.get('ab-bucket')?.value || 'control'
if (request.nextUrl.pathname === '/' && bucket === 'variant') {
return NextResponse.rewrite(new URL('/home-variant', request.url))
}
return NextResponse.next()
}
Rate Limiting Pattern
import { NextResponse } from 'next/server'
import type { NextRequest } from 'next/server'
// Simple in-memory store (use Redis in production)
const rateLimit = new Map<string, { count: number; timestamp: number }>()
export function middleware(request: NextRequest) {
if (!request.nextUrl.pathname.startsWith('/api')) {
return NextResponse.next()
}
const ip = request.headers.get('x-forwarded-for') || 'unknown'
const now = Date.now()
const windowMs = 60000 // 1 minute
const maxRequests = 100
const record = rateLimit.get(ip)
if (!record || now - record.timestamp > windowMs) {
rateLimit.set(ip, { count: 1, timestamp: now })
return NextResponse.next()
}
if (record.count >= maxRequests) {
return NextResponse.json(
{ error: 'Too many requests' },
{ status: 429 }
)
}
record.count++
return NextResponse.next()
}
Middleware Limitations
Middleware runs on the Edge runtime with restrictions:
- No Node.js APIs (fs, path, etc.)
- Limited to Edge-compatible packages
- 1MB code size limit (Vercel)
- Should be fast (affects every request)
Summary
- Middleware runs before every matched request
- Place
middleware.tsin project root - Use
config.matcherto control which routes - Common uses: auth, redirects, headers, rewrites
- Runs on Edge runtime - keep it light and fast
- Can't access Node.js APIs or heavy packages

