Next Middleware¶
Middleware runs at the edge before every matching request. It's your security perimeter , your redirect engine , your A/B testing platform , and your rate limiter - all running in a single file that fires on every request
One middleware.ts file at the root of your project intercepts every request before it reaches your routes. Write it wrong and you block static assets. Write it right and you have a single source of truth for auth , redirects , headers , and geolocation. No other file matters as much for your app's security posture
middleware.ts basics¶
// src/middleware.ts
import { NextResponse } from 'next/server'
import type { NextRequest } from 'next/server'
export function middleware(request: NextRequest) {
// Runs on every matching request before the route handler
// Log the request
console.log(`${request.method} ${request.url}`)
// Continue to the route handler
return NextResponse.next()
}
export const config = {
matcher: '/:path*'
}
The matcher config controls which routes trigger the middleware. Without it , middleware runs on every request including static files
matcher config¶
The matcher is the most important part of your middleware. Get it wrong and:
- Too broad: middleware runs on every image , font , and JS file (wasted CPU)
- Too narrow: routes you meant to protect are accessible without auth
export const config = {
matcher: [
// Match all routes EXCEPT static files and Next.js internals
'/((?!_next/static|_next/image|favicon.ico|.*\\.png$).*)',
]
}
// Match specific paths only
export const config = {
matcher: [
'/dashboard/:path*', // /dashboard , /dashboard/settings , etc
'/admin/:path*', // /admin , /admin/users , etc
'/api/:path*', // /api , /api/users , etc
]
}
// Match with negative lookahead
export const config = {
matcher: [
/*
* Match all request paths except:
* - _next/static (static files)
* - _next/image (image optimization files)
* - favicon.ico (favicon file)
* - public folder files (images , fonts)
*/
'/((?!_next/static|_next/image|favicon.ico|.*\\.(?:svg|png|jpg|jpeg|gif|webp)$).*)',
]
}
The matcher uses regex paths. Test your patterns. A wrong matcher means auth bypass
auth middleware¶
The primary use case: protecting routes behind authentication
// src/middleware.ts
import { auth } from '@/lib/auth'
import { NextResponse } from 'next/server'
export default auth((req) => {
const { pathname } = req.nextUrl
const isLoggedIn = !!req.auth
// Define public routes that don't need auth
const publicRoutes = ['/', '/login', '/register', '/api/auth']
const isPublicRoute = publicRoutes.some(route =>
pathname === route || pathname.startsWith(route + '/')
)
// Allow public routes
if (isPublicRoute) {
return NextResponse.next()
}
// Redirect to login if not authenticated
if (!isLoggedIn) {
const loginUrl = new URL('/login', req.url)
loginUrl.searchParams.set('callbackUrl', pathname)
return NextResponse.redirect(loginUrl)
}
// Check role-based access for admin routes
if (pathname.startsWith('/admin')) {
const role = req.auth?.user?.role
if (role !== 'admin') {
return NextResponse.redirect(new URL('/unauthorized', req.url))
}
}
return NextResponse.next()
})
export const config = {
matcher: '/((?!_next/static|_next/image|favicon.ico|.*\\.png$).*)'
}
redirect logic¶
Middleware is the best place for redirects because they happen before any code loads
// src/middleware.ts
import { NextResponse } from 'next/server'
import type { NextRequest } from 'next/server'
export function middleware(request: NextRequest) {
const { pathname } = request.nextUrl
// Redirect old URLs to new ones
if (pathname.startsWith('/old-blog')) {
const newPath = pathname.replace('/old-blog', '/blog')
return NextResponse.redirect(new URL(newPath, request.url))
}
// Remove trailing slash (SEO best practice)
if (pathname !== '/' && pathname.endsWith('/')) {
const newPath = pathname.slice(0, -1)
return NextResponse.redirect(new URL(newPath, request.url))
}
// Enforce lowercase URLs
const lowerPath = pathname.toLowerCase()
if (pathname !== lowerPath) {
return NextResponse.redirect(new URL(lowerPath, request.url))
}
return NextResponse.next()
}
geolocation-based routing¶
On Vercel , middleware has access to geolocation data
// src/middleware.ts - Vercel edge only
import { NextResponse } from 'next/server'
import type { NextRequest } from 'next/server'
export function middleware(request: NextRequest) {
const { geo } = request
const country = geo?.country || 'US'
// Redirect based on country
if (country === 'DE' && !request.nextUrl.pathname.startsWith('/de')) {
return NextResponse.redirect(new URL('/de' + request.nextUrl.pathname, request.url))
}
// Block traffic from sanctioned countries
const blockedCountries = ['IR', 'KP', 'CU', 'SY']
if (blockedCountries.includes(country || '')) {
return new Response('Access denied', { status: 403 })
}
// Add country header for downstream use
const response = NextResponse.next()
response.headers.set('x-geo-country', country || '')
return response
}
export const config = {
matcher: '/((?!_next/static|_next/image|favicon.ico).*)'
}
Geolocation data is available on Vercel Edge. Self-hosted deployments need to configure their own geo-IP solution
security headers in middleware¶
The cleanest place to set security headers
// src/middleware.ts
import { NextResponse } from 'next/server'
import type { NextRequest } from 'next/server'
export function middleware(request: NextRequest) {
const response = NextResponse.next()
// Security headers
const headers = {
'X-Frame-Options': 'DENY',
'X-Content-Type-Options': 'nosniff',
'Referrer-Policy': 'strict-origin-when-cross-origin',
'X-XSS-Protection': '0', // deprecated but harmless
'Permissions-Policy': 'camera=(), microphone=(), geolocation=()',
'Strict-Transport-Security': 'max-age=31536000; includeSubDomains; preload'
}
Object.entries(headers).forEach(([key, value]) => {
response.headers.set(key, value)
})
return response
}
export const config = {
matcher: '/((?!_next/static|_next/image|favicon.ico).*)'
}
Headers set in middleware apply to every matched response. This is better than setting them in individual route handlers or next.config.ts because middleware covers redirects and error responses too
middleware limitations¶
Middleware runs on the Edge runtime. It has constraints:
- No database access - can't connect to Prisma or any database with native drivers
- No filesystem - can't read files with
fs - No long-running operations - 30-second timeout on Vercel Edge
- Limited API - only Web APIs available (fetch , URL , Request , Response)
- No
req.bodyparsing - middleware can't read the request body (it would consume the stream)
If you need database access in middleware , you're building a route handler , not middleware
prerequisites¶
next_10_security.md - you should understand CSRF , XSS , and CSP concepts
next → next_12_env_config.md