Next.js middleware runs at the edge, before a request hits your page or API routes. It is ideal for auth checks, redirects, rewrites, and header manipulation. Middleware runs in the Edge Runtime for low latency and global distribution.
Creating middleware
Create middleware.ts at the project root:
// middleware.ts
import { NextResponse } from 'next/server';
import type { NextRequest } from 'next/server';
export function middleware(request: NextRequest) {
// Your logic here
return NextResponse.next();
}
The file must be named middleware.ts (or .js) and placed at the root, next to app/ or pages/.
Matching paths
Default behavior
Without configuration, middleware runs on every request. This is usually not what you want.
Using matcher
// middleware.ts
export function middleware(request: NextRequest) {
// ...
}
export const config = {
matcher: [
'/dashboard/:path*',
'/api/protected/:path*',
],
};
Exclude static files
export const config = {
matcher: [
// Match all except static files and Next.js internals
'/((?!_next/static|_next/image|favicon.ico|.*\\.(?:svg|png|jpg|jpeg|gif|webp)$).*)',
],
};
Matcher patterns
| Pattern | Matches |
|---|---|
/dashboard |
Exactly /dashboard |
/dashboard/:path |
/dashboard/anything |
/dashboard/:path* |
/dashboard and all subpaths |
/api/:path+ |
/api with at least one segment |
/(dashboard|admin)/:path* |
/dashboard/* or /admin/* |
Common patterns
Authentication
import { NextResponse } from 'next/server';
import type { NextRequest } from 'next/server';
export function middleware(request: NextRequest) {
const token = request.cookies.get('auth-token');
const isAuthPage = request.nextUrl.pathname.startsWith('/login');
const isProtectedRoute = request.nextUrl.pathname.startsWith('/dashboard');
// Redirect to login if not authenticated
if (isProtectedRoute && !token) {
const loginUrl = new URL('/login', request.url);
loginUrl.searchParams.set('from', request.nextUrl.pathname);
return NextResponse.redirect(loginUrl);
}
// Redirect to dashboard if already authenticated
if (isAuthPage && token) {
return NextResponse.redirect(new URL('/dashboard', request.url));
}
return NextResponse.next();
}
export const config = {
matcher: ['/dashboard/:path*', '/login'],
};
Redirects
export function middleware(request: NextRequest) {
const { pathname } = request.nextUrl;
// Redirect old URLs
if (pathname === '/old-page') {
return NextResponse.redirect(new URL('/new-page', request.url));
}
// Redirect www to non-www
if (request.headers.get('host')?.startsWith('www.')) {
const url = request.nextUrl.clone();
url.host = url.host.replace('www.', '');
return NextResponse.redirect(url, 301);
}
return NextResponse.next();
}
URL rewrites
export function middleware(request: NextRequest) {
const { pathname } = request.nextUrl;
// Rewrite /blog/123 to /posts/123 (URL stays as /blog/123)
if (pathname.startsWith('/blog/')) {
const slug = pathname.replace('/blog/', '');
return NextResponse.rewrite(new URL(`/posts/${slug}`, request.url));
}
return NextResponse.next();
}
A/B testing
export function middleware(request: NextRequest) {
const bucket = request.cookies.get('ab-bucket')?.value;
if (!bucket) {
const newBucket = Math.random() < 0.5 ? 'control' : 'variant';
const response = NextResponse.next();
response.cookies.set('ab-bucket', newBucket, {
maxAge: 60 * 60 * 24 * 7, // 1 week
});
return response;
}
// Rewrite to variant page
if (bucket === 'variant' && request.nextUrl.pathname === '/pricing') {
return NextResponse.rewrite(new URL('/pricing-variant', request.url));
}
return NextResponse.next();
}
Geolocation
export function middleware(request: NextRequest) {
const country = request.geo?.country || 'US';
const city = request.geo?.city;
// Redirect to country-specific page
if (country === 'DE' && !request.nextUrl.pathname.startsWith('/de')) {
return NextResponse.redirect(new URL('/de' + request.nextUrl.pathname, request.url));
}
// Add geo headers for downstream use
const response = NextResponse.next();
response.headers.set('x-user-country', country);
if (city) response.headers.set('x-user-city', city);
return response;
}
Localization
const locales = ['en', 'de', 'fr', 'es'];
const defaultLocale = 'en';
function getLocale(request: NextRequest): string {
const acceptLanguage = request.headers.get('accept-language');
if (!acceptLanguage) return defaultLocale;
const preferred = acceptLanguage.split(',')[0].split('-')[0];
return locales.includes(preferred) ? preferred : defaultLocale;
}
export function middleware(request: NextRequest) {
const { pathname } = request.nextUrl;
// Check if pathname has locale
const pathnameHasLocale = locales.some(
locale => pathname.startsWith(`/${locale}/`) || pathname === `/${locale}`
);
if (pathnameHasLocale) return NextResponse.next();
// Redirect to locale-prefixed path
const locale = getLocale(request);
return NextResponse.redirect(
new URL(`/${locale}${pathname}`, request.url)
);
}
export const config = {
matcher: ['/((?!_next|api|favicon.ico).*)'],
};
Adding headers
export function middleware(request: NextRequest) {
const response = NextResponse.next();
// Security headers
response.headers.set('X-Frame-Options', 'DENY');
response.headers.set('X-Content-Type-Options', 'nosniff');
response.headers.set('Referrer-Policy', 'strict-origin-when-cross-origin');
// Custom headers
response.headers.set('x-request-id', crypto.randomUUID());
return response;
}
Working with cookies
export function middleware(request: NextRequest) {
// Read cookies
const sessionId = request.cookies.get('session-id')?.value;
const allCookies = request.cookies.getAll();
// Set cookies
const response = NextResponse.next();
response.cookies.set('visited', 'true', {
httpOnly: true,
secure: true,
sameSite: 'lax',
maxAge: 60 * 60 * 24 * 365,
});
// Delete cookies
response.cookies.delete('old-cookie');
return response;
}
Edge Runtime limitations
Middleware runs in the Edge Runtime. You cannot use:
| Not available | Alternative |
|---|---|
fs module |
Fetch from API or use edge-compatible storage |
Node.js crypto |
Web Crypto API (crypto.subtle) |
| Native modules | Pure JavaScript alternatives |
| Most database drivers | Edge-compatible clients |
// Use Web Crypto instead of Node crypto
const encoder = new TextEncoder();
const data = encoder.encode('hello');
const hash = await crypto.subtle.digest('SHA-256', data);
Performance tips
- Keep middleware fast - it runs on every matched request
- Use matcher - avoid running on static assets
- Avoid heavy computation - move to route handlers
- Cache when possible - use cookies for persistent state
- Return early - check conditions and return quickly
export function middleware(request: NextRequest) {
// Return early for static paths
if (request.nextUrl.pathname.startsWith('/public')) {
return NextResponse.next();
}
// Only run expensive logic when needed
// ...
}
Summary
Next.js middleware provides:
- Edge execution for low latency
- Request interception before routes
- Redirects and rewrites for routing logic
- Auth checks without page rendering
- Header manipulation for security and tracking
Limitations:
- Edge Runtime only (no Node.js APIs)
- Runs on every matched request
- Keep logic fast and simple
The Next.js middleware docs have the complete API reference.
