background-shape
Next.js 12 Middleware for Auth and Redirects
April 22, 2022 · 6 min read · by Muhammad Amal programming

TL;DR — Next.js 12 middleware runs at the Edge before any page or API route. Use it for auth gates, redirects, geo-based routing, bot blocking, A/B test cookie assignment. Lives in middleware.ts at the project root. Runtime constraints apply (no Node APIs).

After data fetching strategies, the next Next.js 12 feature worth knowing about is middleware. Released in Next.js 12.0 (Oct 2021) and stabilized further in 12.1, middleware lets you run code at the Edge before any page renders or any API route handles. The right tool for cross-cutting concerns that don’t belong in every page.

This post covers the patterns I use middleware for, the constraints to be aware of, and the cases where it’s the wrong answer.

What middleware is

A middleware.ts file at the project root. Exported middleware function that receives the request and returns either a response, a redirect, or “continue”:

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

export function middleware(req: NextRequest) {
  // Inspect the request
  const url = req.nextUrl.clone();
  if (url.pathname.startsWith('/admin')) {
    const session = req.cookies.get('session');
    if (!session) {
      url.pathname = '/login';
      url.searchParams.set('redirect', req.nextUrl.pathname);
      return NextResponse.redirect(url);
    }
  }
  return NextResponse.next();
}

// Scope to specific paths
export const config = {
  matcher: ['/admin/:path*'],
};

The flow:

  1. Request arrives at Next.js
  2. Middleware runs (at the Edge, before any rendering)
  3. Middleware decides: pass through (NextResponse.next()), rewrite, redirect, or return a custom response
  4. If pass-through, normal page/API resolution continues

matcher scopes middleware to specific paths so it doesn’t run on every static asset.

Edge runtime constraints

Middleware runs in the Edge runtime (V8 isolate, not Node). That means:

  • No Node APIs. No fs, no path, no crypto.createHash (use Web Crypto API instead).
  • No node-only npm modules. Most pure-JS packages work; anything with native bindings doesn’t.
  • Small size limit. Middleware bundles are limited (1 MB compressed in 2022). Don’t pull in big dependencies.
  • Short execution time. ~30 ms typical, hard limits enforced by the runtime.
  • Streaming response only. Can’t buffer huge things in memory.

The runtime is what makes middleware fast — it runs at the CDN edge close to the user, in milliseconds. The constraints are the price.

Pattern 1 — auth gating

Most common use:

import { NextResponse } from 'next/server';
import type { NextRequest } from 'next/server';
import { jwtVerify } from 'jose';

const SECRET = new TextEncoder().encode(process.env.SESSION_SECRET);

async function isValidSession(token: string): Promise<boolean> {
  try {
    await jwtVerify(token, SECRET);
    return true;
  } catch {
    return false;
  }
}

export async function middleware(req: NextRequest) {
  const token = req.cookies.get('session')?.value;
  if (!token || !(await isValidSession(token))) {
    const url = req.nextUrl.clone();
    url.pathname = '/login';
    url.searchParams.set('next', req.nextUrl.pathname);
    return NextResponse.redirect(url);
  }
  return NextResponse.next();
}

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

jose is a Web-Crypto-based JWT library. jsonwebtoken (Node-only) doesn’t work here.

The pattern: middleware checks the session cookie, verifies the JWT signature (no DB call), redirects if invalid. Pages that pass through can trust there’s a valid session — they still load user data, but they don’t need to re-check auth.

Pattern 2 — locale / geo redirects

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

export function middleware(req: NextRequest) {
  if (req.nextUrl.pathname !== '/') return NextResponse.next();

  const country = req.geo?.country?.toLowerCase() ?? 'us';
  const supported = ['us', 'gb', 'id', 'jp'];
  const locale = supported.includes(country) ? country : 'us';

  if (locale !== 'us') {
    const url = req.nextUrl.clone();
    url.pathname = `/${locale}`;
    return NextResponse.redirect(url);
  }
  return NextResponse.next();
}

export const config = {
  matcher: '/',
};

req.geo (on Vercel; populated by the platform) gives country, city, region. Use it to route landing-page traffic to localized variants.

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

export function middleware(req: NextRequest) {
  let bucket = req.cookies.get('ab-bucket')?.value;
  if (!bucket) {
    bucket = Math.random() < 0.5 ? 'A' : 'B';
    const res = NextResponse.next();
    res.cookies.set('ab-bucket', bucket, {
      maxAge: 60 * 60 * 24 * 30,
      path: '/',
    });
    return res;
  }
  return NextResponse.next();
}

First visit gets a random bucket. Subsequent visits get the same bucket via the cookie. Pages read req.cookies to render the right variant.

For real A/B testing you’d use a feature flag service (LaunchDarkly, GrowthBook, Statsig). The middleware pattern is for lightweight in-house experiments.

Pattern 4 — bot blocking / rate limiting (caution)

const SUSPICIOUS_UAS = [/curl/i, /wget/i, /python-requests/i];

export function middleware(req: NextRequest) {
  const ua = req.headers.get('user-agent') || '';
  if (SUSPICIOUS_UAS.some(re => re.test(ua))) {
    return new NextResponse('forbidden', { status: 403 });
  }
  return NextResponse.next();
}

Naive but useful for blocking the worst offenders. Real rate limiting needs a distributed counter (Redis); the Edge runtime is stateless so you can’t keep counters in middleware itself.

For real protection, use Cloudflare/Vercel WAF or your CDN’s rate limiting features. Middleware is the wrong layer for serious abuse prevention.

Rewriting vs redirecting

Two operations to know:

// Redirect: browser navigates to the new URL (visible in the bar)
return NextResponse.redirect(new URL('/login', req.url));

// Rewrite: serve content from a different path without changing the URL
return NextResponse.rewrite(new URL('/maintenance', req.url));

Use redirect for auth flows (you want the URL bar to change). Use rewrite for serving alternate content under the same URL (maintenance mode, A/B variants, geo content).

What middleware is NOT for

  • Heavy data fetching — round-trips to your DB exceed the Edge’s latency budget. Do data fetching in pages.
  • Long-running work — Edge runs are short. Background jobs go elsewhere.
  • Stateful logic — no in-memory state between requests. Cookies, headers, or external store.
  • Replacing your real auth backend — verify JWT signatures (cheap, Edge-friendly) but issue and store tokens server-side.
  • CSRF token generation — needs a stable secret + crypto; doable but messy. Better in an API route.

Common Pitfalls

Importing jsonwebtoken in middleware. Node-only. Build fails with “module not found.” Use jose or jws-jws-jwa instead.

Forgetting matcher. Without it, middleware runs on every request including static assets — wasteful and slow. Always scope.

Bundling huge dependencies. Middleware has a 1 MB size limit. Importing the wrong thing blows the budget.

Calling external APIs from middleware. The latency stacks on every request. Cache aggressively or rethink.

Mutating request headers expecting them to flow to the page. Some headers flow; some don’t. Test specifically what you need.

Treating middleware as authoritative auth. Middleware should be a fast filter. The real auth check (looking up user, checking permissions for the specific action) happens in the page or API route.

Forgetting that middleware runs on every matched request, including prefetched ones. Next.js prefetches Links on hover. Your middleware runs for those prefetches. Auth checks should be idempotent.

Wrapping Up

Middleware is the right tool for auth gates, redirects, and edge-level routing. Tight constraints (Edge runtime, size, time) limit it to specific use cases. Use it for the cross-cutting concerns that don’t belong in pages; reach elsewhere for everything else. Monday: image optimization with next/image — the other Next.js feature that punches above its config weight.