Middleware in Next.js runs before a request is processed — before the cache is checked, before the route renders, before any server code executes. This makes it the right place for authentication checks, redirects, request modification, and A/B testing logic that needs to apply across multiple routes without being duplicated in each one.
Here's the complete middleware pattern, including auth guards, geographic redirects, A/B testing with cookies, and the performance considerations that determine when middleware is and isn't the right tool — including patterns used in the infrastructure behind ai caricature generation workflows.
Creating Middleware
Create middleware.ts at the project root (same level as app/):
// middleware.ts
import { NextResponse } from 'next/server';
import type { NextRequest } from 'next/server';
export function middleware(request: NextRequest) {
return NextResponse.next();
}
// Configure which paths middleware runs on
export const config = {
matcher: [
'/((?!_next/static|_next/image|favicon.ico).*)',
],
};
The matcher is critical — middleware that runs on static assets and image optimization requests adds latency for no reason.
Authentication Guard
The most common middleware use: blocking unauthenticated access to protected routes.
// middleware.ts
import { NextResponse } from 'next/server';
import type { NextRequest } from 'next/server';
const protectedPaths = ['/dashboard', '/account', '/settings'];
const authPath = '/login';
export function middleware(request: NextRequest) {
const { pathname } = request.nextUrl;
const isProtected = protectedPaths.some(path =>
pathname.startsWith(path)
);
if (!isProtected) {
return NextResponse.next();
}
const token = request.cookies.get('auth-token')?.value;
if (!token) {
const loginUrl = new URL(authPath, request.url);
loginUrl.searchParams.set('from', pathname);
return NextResponse.redirect(loginUrl);
}
// Optionally validate token structure here
// For full JWT validation, use an Edge-compatible library
return NextResponse.next();
}
export const config = {
matcher: ['/((?!_next/static|_next/image|favicon.ico|api/auth).*)'],
};
The from parameter preserves the original destination — after login, the user can be redirected back to where they were going.
JWT Validation in Middleware
For proper JWT validation, use an Edge-compatible library (standard Node.js crypto isn't available in middleware's Edge runtime):
// middleware.ts
import { NextResponse } from 'next/server';
import type { NextRequest } from 'next/server';
import { jwtVerify } from 'jose'; // Edge-compatible JWT library
const JWT_SECRET = new TextEncoder().encode(
process.env.JWT_SECRET!
);
async function validateToken(token: string): Promise<boolean> {
try {
await jwtVerify(token, JWT_SECRET);
return true;
} catch {
return false;
}
}
export async function middleware(request: NextRequest) {
const { pathname } = request.nextUrl;
if (!pathname.startsWith('/dashboard')) {
return NextResponse.next();
}
const token = request.cookies.get('auth-token')?.value;
if (!token || !(await validateToken(token))) {
return NextResponse.redirect(new URL('/login', request.url));
}
return NextResponse.next();
}
jose is Edge-compatible unlike jsonwebtoken, which requires Node.js crypto.
Geographic Redirects
Middleware can read the geo data that Vercel and other platforms inject into requests:
export function middleware(request: NextRequest) {
const country = request.geo?.country;
if (country === 'DE') {
return NextResponse.redirect(new URL('/de', request.url));
}
if (country === 'JP') {
return NextResponse.redirect(new URL('/jp', request.url));
}
return NextResponse.next();
}
request.geo is populated by Vercel's edge network. On other platforms, use the CF-IPCountry header (Cloudflare) or similar. In development, request.geo is undefined.
A/B Testing With Cookies
Middleware is ideal for A/B testing because it assigns the variant before any rendering occurs, ensuring the user consistently sees the same variant across page loads:
import { NextResponse } from 'next/server';
import type { NextRequest } from 'next/server';
const AB_COOKIE = 'ab-variant';
const VARIANTS = ['control', 'treatment'] as const;
type Variant = typeof VARIANTS[number];
function getVariant(request: NextRequest): Variant {
const existing = request.cookies.get(AB_COOKIE)?.value;
if (existing === 'control' || existing === 'treatment') {
return existing;
}
return Math.random() < 0.5 ? 'control' : 'treatment';
}
export function middleware(request: NextRequest) {
const { pathname } = request.nextUrl;
if (!pathname.startsWith('/pricing')) {
return NextResponse.next();
}
const variant = getVariant(request);
const response = NextResponse.next();
// Persist the variant
response.cookies.set(AB_COOKIE, variant, {
maxAge: 60 * 60 * 24 * 30, // 30 days
httpOnly: true,
sameSite: 'lax',
});
// Pass variant to the page via header
response.headers.set('x-ab-variant', variant);
return response;
}
In the page component, read the header to determine which variant to render:
// app/pricing/page.tsx
import { headers } from 'next/headers';
export default function PricingPage() {
const headersList = headers();
const variant = headersList.get('x-ab-variant') ?? 'control';
if (variant === 'treatment') {
return <PricingV2 />;
}
return <PricingV1 />;
}
Request Modification and Header Injection
Middleware can modify requests before they reach route handlers — useful for injecting context that components or API routes need:
export function middleware(request: NextRequest) {
const requestHeaders = new Headers(request.headers);
// Add the request URL for server components that need it
requestHeaders.set('x-pathname', request.nextUrl.pathname);
return NextResponse.next({
request: { headers: requestHeaders },
});
}
The Matcher — Getting It Right
The matcher config determines which requests trigger middleware:
export const config = {
matcher: [
// Match all paths except static files and Next.js internals
'/((?!_next/static|_next/image|favicon.ico).*)',
// Or: match specific paths only
'/dashboard/:path*',
'/account/:path*',
// Or: match API routes
'/api/:path*',
],
};
Incorrect matchers are the most common middleware bug — middleware running on _next/static paths adds latency to every static asset request.
Performance Considerations
Middleware runs on the Edge runtime, which has constraints:
- No Node.js APIs (no
fs, no nativecrypto) - No npm packages that use Node.js internals
- Execution time limits (typically under 50ms)
For heavy operations — database queries, complex business logic — don't put them in middleware. Middleware should handle fast operations: cookie reads, header inspection, simple redirects. Anything that requires database access belongs in the route handler or Server Component.
When NOT to Use Middleware
Middleware is not the right place for:
Database queries. Every request that triggers middleware will wait for the database call. Use middleware for fast cookie/header checks; move database calls to Server Components or route handlers where caching applies.
Heavy business logic. Complex calculations or multi-step operations belong in server-side code where they can be cached and where the full Node.js environment is available.
Per-request secret handling. Don't access environment variables containing secrets in middleware for operations that could be handled with a simple cookie check. Keep middleware logic minimal and fast.
Image optimization. The _next/image path should be excluded from middleware matchers — running middleware on image optimization requests adds latency to every image load.
The mental model: middleware handles routing decisions. Route handlers and Server Components handle business logic and data fetching.
Summary
Middleware intercepts requests before rendering — right for auth checks, redirects, A/B variant assignment, and request header injection. Create at project root as middleware.ts. Use the Edge-compatible jose for JWT validation. Always configure matcher to exclude static assets. Keep logic fast — no database calls, no heavy computation.
Testing Middleware Locally
Middleware runs in the Edge runtime even during local development with next dev. Test by:
# Check which paths trigger middleware with a simple log
export function middleware(request: NextRequest) {
console.log('[Middleware]', request.method, request.nextUrl.pathname);
return NextResponse.next();
}
For auth middleware specifically: test with an expired/invalid token, a missing token, and a valid token to cover the three cases that matter. Also test that the from redirect parameter round-trips correctly through the login flow.
Top comments (0)