You are currently viewing Next.js Middleware: Practical Patterns for Auth, Redirects, and A/B Testing
Photo by cottonbro studio on Pexels

Next.js Middleware: Practical Patterns for Auth, Redirects, and A/B Testing

  • Post category:Next.js
  • Post comments:0 Comments
  • Reading time:6 mins read
  • Post last modified:September 11, 2026

Next.js Middleware lets you run code before a request completes, intercepting it at the edge to rewrite, redirect, or modify headers before any page or API route ever renders. It’s tempting to treat it like a general-purpose backend hook, but Middleware runs in a constrained Edge Runtime, on every matching request, which means the patterns that work well here are narrower than a typical Node.js request handler. This guide walks through three of the most common production use cases — authentication gating, redirects, and A/B test bucketing — along with the pitfalls that trip people up.

What Middleware Actually Is

A middleware.ts (or .js) file placed at the root of your project (or inside src/) exports a function that runs before the request is matched to a route. It executes in the Edge Runtime by default — a V8 isolate environment, not full Node.js — so APIs like fs, most native Node modules, and long-running database drivers aren’t available. Think of it as a fast filter that decides: let this request through, redirect it somewhere else, rewrite it to a different path, or modify its headers.

// middleware.ts
import { NextResponse } from 'next/server';
import type { NextRequest } from 'next/server';

export function middleware(request: NextRequest) {
  return NextResponse.next();
}

export const config = {
  matcher: ['/dashboard/:path*', '/settings/:path*'],
};

The matcher config is important: without it, middleware runs on every request, including static assets and API routes you probably don’t want to touch. Scoping the matcher keeps latency down and avoids surprising side effects on routes you forgot about.

Pattern 1: Authentication Gating

The most common use case is redirecting unauthenticated users away from protected routes before any page code runs. Middleware can read cookies directly, which makes a quick session check cheap — but it should stay a quick check. Don’t try to validate a JWT signature with a heavy crypto library or hit your primary database inside middleware; verify a lightweight session token instead, and let the actual page or a server component do deeper authorization if needed.

// middleware.ts
import { NextResponse } from 'next/server';
import type { NextRequest } from 'next/server';

export function middleware(request: NextRequest) {
  const sessionToken = request.cookies.get('session')?.value;

  if (!sessionToken) {
    const loginUrl = new URL('/login', request.url);
    loginUrl.searchParams.set('from', request.nextUrl.pathname);
    return NextResponse.redirect(loginUrl);
  }

  return NextResponse.next();
}

export const config = {
  matcher: ['/dashboard/:path*', '/account/:path*'],
};

Notice the from query parameter — it’s a small touch that lets your login page redirect the user back to where they were headed after they authenticate, instead of dumping them on a generic dashboard. This is a detail that’s easy to skip but noticeably improves the login flow.

If you need to verify a signed token (say, a JWT issued by your auth provider) rather than just checking for a cookie’s presence, libraries like jose work in the Edge Runtime because they’re built on Web Crypto APIs rather than Node’s crypto module. Avoid libraries that assume a Node.js environment — they’ll fail at build or runtime with cryptic errors about missing globals.

Pattern 2: Geo and Locale Redirects

Middleware has access to request.geo (on supported hosting platforms) and request headers like accept-language, making it a natural place to redirect users to a localized version of your site without a client-side flash of the wrong content.

export function middleware(request: NextRequest) {
  const { pathname } = request.nextUrl;
  const supportedLocales = ['en', 'de', 'fr', 'ja'];
  const hasLocale = supportedLocales.some(
    (locale) => pathname.startsWith(`/${locale}/`) || pathname === `/${locale}`
  );

  if (hasLocale) return NextResponse.next();

  const preferred = request.headers
    .get('accept-language')
    ?.split(',')[0]
    .split('-')[0];

  const locale = supportedLocales.includes(preferred || '') ? preferred : 'en';
  const url = request.nextUrl.clone();
  url.pathname = `/${locale}${pathname}`;
  return NextResponse.redirect(url);
}

A subtlety worth calling out: redirecting based on accept-language on every visit can be annoying for a user who explicitly wants English content on a German-language browser. Most production sites pair this with a cookie that remembers an explicit locale choice and skips the header-based guess once it’s set.

Pattern 3: A/B Test Bucketing

Because middleware runs before rendering, it’s a good place to assign a user to an experiment bucket and pass that decision downstream via a cookie or a rewritten URL — avoiding the layout shift you’d get from bucketing in client-side JavaScript after the page has already painted.

export function middleware(request: NextRequest) {
  const response = NextResponse.next();
  const existingBucket = request.cookies.get('ab-bucket')?.value;

  if (existingBucket) return response;

  const bucket = Math.random() < 0.5 ? 'control' : 'variant';
  response.cookies.set('ab-bucket', bucket, {
    maxAge: 60 * 60 * 24 * 30,
    path: '/',
  });

  return response;
}

Your page components (or an analytics client) then read the ab-bucket cookie to decide what to render or which event to log. Keeping the bucketing decision in middleware, rather than scattered across individual pages, means every route sees a consistent assignment and you have one place to change the split ratio.

Rewrites vs. Redirects

It's worth being precise about the difference, since mixing them up is a common source of bugs. A redirect (NextResponse.redirect) sends the browser a 307/308 response and changes the URL the user sees. A rewrite (NextResponse.rewrite) serves different content while keeping the original URL in the address bar — useful for things like serving a different page for a specific A/B variant or proxying a path to an internal API without exposing that internal path publicly.

export function middleware(request: NextRequest) {
  const bucket = request.cookies.get('ab-bucket')?.value;

  if (bucket === 'variant') {
    const url = request.nextUrl.clone();
    url.pathname = '/landing-variant';
    return NextResponse.rewrite(url);
  }

  return NextResponse.next();
}

Performance Considerations

Middleware runs on every matched request, on the critical path, before your page starts rendering. A few practical guidelines keep it from becoming a bottleneck:

  • Scope matcher as tightly as possible — don't run auth checks on routes that don't need them.
  • Avoid network calls (database queries, third-party API calls) inside middleware when you can. If you must call an external service, cache the result in a cookie or header so you're not doing it on every request.
  • Keep logic synchronous and lightweight; the Edge Runtime has stricter execution limits than a typical Node.js server.
  • Remember that middleware runs before static asset caching decisions in some configurations — test that you haven't accidentally made static files pass through your logic.

Conclusion

Next.js Middleware is most valuable as a thin, fast decision layer at the edge — redirecting, rewriting, or tagging requests based on cheap checks like cookies and headers, not as a place for business logic or database access. Auth gating, locale redirects, and A/B bucketing all fit that model well because they're binary decisions that need to happen before rendering starts. When you reach for middleware, ask whether the check can be done with data already available on the request; if it needs a database round trip or heavy computation, that logic almost always belongs in a server component or API route instead.

0 0 votes
Article Rating
Subscribe
Notify of
guest
0 Comments
Oldest
Newest Most Voted