Middleware Patterns
Run code on the Edge before a request completes: auth gates, redirects, rewrites, geo, and header injection — with a matcher that keeps it cheap.
Middleware is a single middleware.ts at the project root that runs before a request is completed, on Next's Edge runtime. It is your one chance to inspect and reshape a request globally before any route renders.
What it is good at
- Auth gating — read a session cookie and redirect unauthenticated users away from protected paths.
- Rewrites — serve one URL from another path invisibly (A/B tests, multi-tenant subdomains, i18n).
- Redirects — canonical host, locale detection, legacy URL maps.
- Request/response headers — inject a nonce, a request id, or geo data.
The matcher is not optional
Without a config.matcher, middleware runs on every request including static assets — pure overhead. Scope it. The standard incantation excludes _next/static, _next/image, and the favicon so middleware only runs on real page/route traffic.
Edge runtime constraints
Middleware runs on the Edge: no Node.js APIs, no filesystem, no native database drivers, and a strict CPU budget. Keep it to cookie/header logic and cheap checks. Do the real work (verify a JWT signature, hit the DB) inside the route or a Server Component, and treat middleware as a fast coarse gate. Also: never trust middleware alone for authorization — a cookie's mere presence is not proof of a valid session.
Example
// middleware.ts (project root)
import { NextResponse, type NextRequest } from 'next/server';
export function middleware(request: NextRequest) {
const session = request.cookies.get('session')?.value;
const { pathname } = request.nextUrl;
// Coarse gate: bounce anonymous users off protected areas
if (pathname.startsWith('/dashboard') && !session) {
const url = request.nextUrl.clone();
url.pathname = '/login';
url.searchParams.set('from', pathname);
return NextResponse.redirect(url);
}
// Pass a request id downstream via headers
const res = NextResponse.next();
res.headers.set('x-request-id', crypto.randomUUID());
return res;
}
export const config = {
// Run everywhere EXCEPT static assets and image optimizer
matcher: ['/((?!_next/static|_next/image|favicon.ico).*)'],
};When to use it
- An auth middleware checks a JWT in cookies on every /app/* request and redirects to /login when it is absent or expired.
- A geo middleware reads the cf-ipcountry header and rewrites the URL to the correct locale for the visitor's region.
- An A/B test middleware randomly assigns a bucket cookie and rewrites the path to /experiment-a or /experiment-b.
More examples
Auth gate with matcher config
The matcher limits the middleware to specific path prefixes, keeping it fast by skipping static assets.
// middleware.ts
import { NextRequest, NextResponse } from 'next/server';
import { verifyToken } from '@/lib/auth';
export async function middleware(req: NextRequest) {
const token = req.cookies.get('token')?.value;
const valid = token ? await verifyToken(token) : false;
if (!valid) {
return NextResponse.redirect(new URL('/login', req.url));
}
return NextResponse.next();
}
export const config = { matcher: ['/app/:path*', '/api/private/:path*'] };Geo-based locale rewrite
req.geo gives Vercel edge geo data; NextResponse.rewrite serves localised content without a browser redirect.
// middleware.ts
import { NextRequest, NextResponse } from 'next/server';
const LOCALES = ['en', 'fr', 'de'];
export function middleware(req: NextRequest) {
const country = req.geo?.country ?? 'US';
const locale = country === 'FR' ? 'fr' : country === 'DE' ? 'de' : 'en';
const url = req.nextUrl.clone();
url.pathname = `/${locale}${req.nextUrl.pathname}`;
return NextResponse.rewrite(url);
}
export const config = { matcher: ['/((?!_next|api).*)'] };A/B test bucket assignment
The middleware assigns and persists a bucket cookie then rewrites the path, giving consistent experiences per visitor.
// middleware.ts
import { NextRequest, NextResponse } from 'next/server';
export function middleware(req: NextRequest) {
const bucket = req.cookies.get('ab-bucket')?.value ?? (Math.random() < 0.5 ? 'a' : 'b');
const url = req.nextUrl.clone();
url.pathname = `/experiment-${bucket}${req.nextUrl.pathname}`;
const res = NextResponse.rewrite(url);
res.cookies.set('ab-bucket', bucket, { maxAge: 60 * 60 * 24 * 30 });
return res;
}
Discussion