Skip to content

Next.js Middleware: The Five Patterns That Earn Their Keep

Middleware runs on every request. Use it for things that have to happen before the page renders. Stop using it for everything else.

John Cravey with EleviFounder4 min read

Next.js middleware runs at the edge before any route handler. It’s the right place for things that have to happen before the page renders — redirecting users, setting headers, rewriting URLs. It’s the wrong place for almost anything else, and the wrong-place uses are where we see middleware silently slowing down whole client sites.

What middleware is and what it isn’t

Middleware is a single function in `middleware.ts` at the root of your project. It receives every matching request and returns either a Response (redirect/rewrite/pass-through) or modifies headers. It runs at the edge, which means it’s fast but limited — no Node-only APIs, no large dependencies, no database calls (no good ones, anyway).

Middleware is not the right place for data fetching, complex business logic, or anything that needs Node APIs. If you find yourself wanting `fs.readFile` in middleware, you’ve picked the wrong layer.

Pattern 1: geolocation-based redirects

Cloudflare and Vercel both expose the visitor’s country in the request headers. Use it to route US visitors to a US-specific page, EU visitors to a GDPR-compliant variant, and so on. We use this on one e-commerce client to redirect EU users to the EU storefront automatically.

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

export function middleware(req: NextRequest) {
  const country = req.geo?.country ?? "US";
  if (country === "GB" || country === "DE" || country === "FR") {
    return NextResponse.redirect(new URL("/eu", req.url));
  }
}

export const config = { matcher: ["/((?!api|_next|.*\\..*).*)"] };

Pattern 2: auth checks for protected routes

Check for an auth cookie. If present, let the request through. If absent on a protected route, redirect to login. Do not validate the JWT in middleware unless you have to — JWT validation needs the secret, and middleware secrets get rotated awkwardly. We do a simple presence check in middleware and a full validation in the server component or route handler.

export function middleware(req: NextRequest) {
  const token = req.cookies.get("session")?.value;
  if (!token && req.nextUrl.pathname.startsWith("/admin")) {
    const url = req.nextUrl.clone();
    url.pathname = "/login";
    url.searchParams.set("next", req.nextUrl.pathname);
    return NextResponse.redirect(url);
  }
}

Pattern 3: A/B test variant selection

Assign a variant cookie on first visit. Rewrite subsequent requests to the variant page based on the cookie. This keeps the test logic out of the React render tree and gives every visitor a stable variant for the duration of their session.

Pattern 4: bot detection and rate limiting

Use the request headers (user agent, IP, country) to short-circuit obvious bot traffic before it hits your origin. We pair this with Cloudflare’s bot management for the heavy lifting and use middleware only for the last-mile filter on routes Turnstile doesn’t cover (like the API routes for our analytics endpoint).

Pattern 5: locale and language routing

Read the `accept-language` header or a locale cookie, rewrite to the matching locale prefix. Cleaner than client-side detection — the URL reflects the locale immediately, no flash of wrong-language content, no SEO downside.

The matcher: scope your middleware tightly

Middleware runs on every matching request — including static assets if you’re not careful. The default matcher pattern (`"/((?!api|_next|.*\\..*).*)"`) excludes API routes, Next internals, and anything with a file extension (images, fonts, JS). Use a matcher tight enough that you’re not running edge code on every image request, but loose enough that you cover the routes you care about.

What not to do in middleware

  • Database calls. Middleware runs at the edge with no Node APIs; even an HTTP call to your DB adds 100–300ms of latency.
  • Heavy logic. The whole point of edge middleware is single-digit-ms response. If your middleware does more than 50 lines of computation, it belongs in a route handler.
  • JWT validation with crypto.subtle. Possible but slow and awkward. Do it in the route handler.
  • Setting hundreds of cookies. Headers have a size limit and middleware errors silently if you blow past it.

Debugging middleware

Middleware errors don’t always surface — a malformed Response returns a 500 with no detail by default. Wrap your middleware body in a try/catch and log errors to your observability platform. We log to GA4 as custom events for non-fatal middleware decisions (variant assignments, geo redirects) so we can see what’s happening at the edge in aggregate.

When NOT to use middleware at all

If your need is per-route logic that runs after data fetching, that’s the route handler, not middleware. If your need is to add headers to all responses, that’s the deploy platform’s configuration (Vercel’s `vercel.json`, Coolify’s reverse proxy), not middleware. If your need is rewriting URLs based on database state, that’s SSR, not middleware. The rule of thumb: middleware is for fast, request-level decisions that can be made from headers alone.

How this lands across FH client work

Most FH client sites use middleware for exactly one thing: a single redirect for legacy URLs. The rest of their auth, A/B, and geo logic lives in route handlers or in the deploy platform. The bias is toward keeping middleware boring and small. If you’re shipping 200 lines of middleware on a marketing site, book an audit — there’s almost certainly a faster way to get the same behavior without paying edge latency on every request.

Written by
John Cravey
Founder

Founder of Frontend Horizon. Writes most of the long-form work on the FH blog.

Newer post
Google Search Console: AI Overviews Data and What to Do About the Clicks You’re Losing
Older post
The Death of Cookie-Based Tracking: What Replaces It in 2026
Keep reading

More from the blog

Next.js·6 min

Next.js 16.1 in Production: The Migration Playbook We Run on Every FH Site

Next 16.1 is the lean target. Here’s the exact migration we run, what breaks, and what to delete after.

Next.js·4 min

Server Components vs Client Components: The Mental Model That Stops You Reaching for ‘use client’

Most teams add ‘use client’ because they’re scared. The bundle pays for it.

Cloudflare·4 min

Cloudflare Turnstile: The CAPTCHA That Doesn’t Make Your Users Hate You

reCAPTCHA hurts conversion. Turnstile doesn’t. Here’s the wiring that keeps your forms spam-free without the click-the-bicycles ritual.