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
fetchto 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 .