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

Example · typescript
// 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.

Example · ts
// 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.

Example · ts
// 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.

Example · ts
// 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

  • Be the first to comment on this lesson.