What middleware is
A single function in middleware.ts at your project root that intercepts every request. It runs on the Edge Runtime — a lightweight V8 isolate, not a full Node.js environment.
// middleware.ts
import { NextRequest, NextResponse } from 'next/server';
export function middleware(request: NextRequest) {
// Runs before every matched request
return NextResponse.next();
}
export const config = {
matcher: ['/dashboard/:path*', '/api/:path*'],
};
The matcher limits which routes trigger the middleware. Without it, middleware runs on every request including static assets.
Authentication at the edge
The most common use case. Check for a session token and redirect unauthenticated users:
import { NextRequest, NextResponse } from 'next/server';
import { jwtVerify } from 'jose';
const SECRET = new TextEncoder().encode(process.env.JWT_SECRET!);
export async function middleware(request: NextRequest) {
const token = request.cookies.get('session')?.value;
if (!token) {
return NextResponse.redirect(new URL('/login', request.url));
}
try {
const { payload } = await jwtVerify(token, SECRET);
const headers = new Headers(request.headers);
headers.set('x-user-id', payload.sub as string);
headers.set('x-user-role', payload.role as string);
return NextResponse.next({ headers });
} catch {
return NextResponse.redirect(new URL('/login', request.url));
}
}
The JWT is verified at the edge. If valid, user info is passed to the page via headers. The page never needs to verify the token itself.
Geolocation-based routing
Edge functions have access to the request's geolocation:
export function middleware(request: NextRequest) {
const country = request.geo?.country || 'US';
if (country === 'DE' && !request.nextUrl.pathname.startsWith('/de')) {
return NextResponse.redirect(new URL('/de' + request.nextUrl.pathname, request.url));
}
return NextResponse.next();
}
A/B testing without client-side flicker
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 * 30 });
return response;
}
if (bucket === 'variant' && request.nextUrl.pathname === '/pricing') {
return NextResponse.rewrite(new URL('/pricing-v2', request.url));
}
return NextResponse.next();
}
The rewrite is transparent — the URL stays /pricing but the content comes from /pricing-v2. No client-side JavaScript, no layout shift, no flicker.
Edge Runtime limitations
The Edge Runtime is not Node.js. You cannot use:
fs,path, or any Node.js built-in modules- npm packages that depend on Node.js APIs
- Long-running processes (middleware has a ~25ms budget on Vercel)
Keep middleware thin. Verify a token, set a header, redirect — that is it. Heavy logic belongs in your API routes or server components.
Rate limiting at the edge
import { NextRequest, NextResponse } from 'next/server';
const rateLimit = new Map<string, { count: number; resetTime: 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 window = 60000; // 1 minute
const limit = 100;
const entry = rateLimit.get(ip);
if (!entry || now > entry.resetTime) {
rateLimit.set(ip, { count: 1, resetTime: now + window });
return NextResponse.next();
}
if (entry.count >= limit) {
return new NextResponse('Too Many Requests', { status: 429 });
}
entry.count++;
return NextResponse.next();
}
Note: this in-memory approach only works for single-instance deployments. For distributed rate limiting, use Upstash Redis or a similar edge-compatible store.
