Middleware cheatsheet.

Basic

// middleware.ts (project root, NOT in app/)
import { NextResponse, type NextRequest } from "next/server";

export function middleware(req: NextRequest) {
  console.log("middleware:", req.nextUrl.pathname);
  return NextResponse.next();
}

Runs before requests reach the route handler/page.

Matcher

export const config = {
  matcher: [
    "/dashboard/:path*",
    "/api/:path*",
    "/((?!_next/static|_next/image|favicon.ico).*)",   // all except static
  ],
};

Without matcher, middleware runs on every request.

Auth gate

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

export const config = { matcher: ["/dashboard/:path*"] };

Redirect

return NextResponse.redirect(new URL("/login", req.url));
return NextResponse.redirect(new URL("/x", req.url), 308);   // permanent

Rewrite (URL stays, content swapped)

return NextResponse.rewrite(new URL("/internal/x", req.url));

User sees /foo, server serves /internal/x.

Setting headers

const res = NextResponse.next();
res.headers.set("x-custom", "value");
return res;

Setting cookies

const res = NextResponse.next();
res.cookies.set("session", "...", {
  httpOnly: true,
  secure: true,
  sameSite: "lax",
  maxAge: 60 * 60 * 24,
});
return res;

Reading cookies

const value = req.cookies.get("name")?.value;
req.cookies.has("name");

Geolocation / IP

// Next 15+: from request headers
const country = req.headers.get("x-vercel-ip-country");
const ip = req.headers.get("x-forwarded-for")?.split(",")[0];

Conditional logic by host

export function middleware(req: NextRequest) {
  const host = req.headers.get("host");
  
  if (host === "admin.example.com") {
    return NextResponse.rewrite(new URL(`/admin${req.nextUrl.pathname}`, req.url));
  }
  return NextResponse.next();
}

Multi-tenancy / subdomain routing.

A/B testing

export function middleware(req: NextRequest) {
  if (req.nextUrl.pathname !== "/") return;
  
  let bucket = req.cookies.get("bucket")?.value;
  if (!bucket) {
    bucket = Math.random() < 0.5 ? "a" : "b";
  }
  
  const res = NextResponse.rewrite(new URL(`/variants/${bucket}`, req.url));
  res.cookies.set("bucket", bucket, { maxAge: 60 * 60 * 24 * 30 });
  return res;
}

CORS

export function middleware(req: NextRequest) {
  if (req.nextUrl.pathname.startsWith("/api/")) {
    const res = NextResponse.next();
    res.headers.set("Access-Control-Allow-Origin", "https://app.example.com");
    res.headers.set("Access-Control-Allow-Methods", "GET, POST, PUT, DELETE");
    return res;
  }
  return NextResponse.next();
}

Or preflight:

if (req.method === "OPTIONS") {
  return new Response(null, {
    status: 204,
    headers: {
      "Access-Control-Allow-Origin": "...",
      "Access-Control-Allow-Methods": "GET, POST",
      "Access-Control-Allow-Headers": "Content-Type, Authorization",
    },
  });
}

Security headers

const res = NextResponse.next();
res.headers.set("X-Frame-Options", "DENY");
res.headers.set("X-Content-Type-Options", "nosniff");
res.headers.set("Referrer-Policy", "strict-origin-when-cross-origin");
res.headers.set("Permissions-Policy", "camera=(), microphone=()");
return res;

For CSP, prefer next.config.js headers() or nonce-based pattern.

Rate limiting

import { Ratelimit } from "@upstash/ratelimit";
import { Redis } from "@upstash/redis";

const rl = new Ratelimit({
  redis: Redis.fromEnv(),
  limiter: Ratelimit.slidingWindow(10, "10 s"),
});

export async function middleware(req: NextRequest) {
  if (!req.nextUrl.pathname.startsWith("/api/")) return;
  
  const ip = req.headers.get("x-forwarded-for") ?? "anon";
  const { success } = await rl.limit(ip);
  
  if (!success) {
    return new NextResponse("Too many requests", { status: 429 });
  }
  return NextResponse.next();
}

Edge runtime constraints

Middleware runs at the Edge:

  • Limited Node API (no fs, child_process).
  • Smaller bundle size budget.
  • Faster cold start.

Use only lightweight logic. Heavy work belongs in route handlers / server components.

Async middleware

export async function middleware(req: NextRequest) {
  const valid = await verifyToken(req.cookies.get("token")?.value);
  if (!valid) return NextResponse.redirect(new URL("/login", req.url));
  return NextResponse.next();
}

Combining matchers and logic

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

Negative lookahead pattern excludes paths.

Common mistakes

  • Heavy work in middleware → slow every request.
  • Forgetting matcher → runs on static asset requests.
  • Modifying request — middleware can only modify responses.
  • Calling DB directly — Edge runtime can’t (mostly).
  • Using fetch to API on same Next app inside middleware — recursive.

Read this next

If you want my middleware patterns + rate limiter, they’re at rajpoot.dev .


Building something AI-, backend-, or data-heavy and want a second pair of eyes? I do consulting and freelance work — see my projects and ways to reach me at rajpoot.dev .