On this page · sections
Middleware, Auth & Request APIs
Middleware runs at the edge before a request reaches your route handlers or RSC render. It is the right layer for auth gates, geo redirects, A/B routing, and header injection — not for heavy DB work. Request APIs (cookies, headers, redirect, rewrite) let Server Components and middleware read and shape each request.
| Layer | Runs when | Best for | Avoid |
|---|---|---|---|
| Middleware | Before route match/render | Auth redirect, locale, bot blocking | DB queries, business logic |
| Server Component | During RSC render | Session read, data fetch | Client-only APIs |
| Server Action | POST mutation | Validate session, mutate | Long-running jobs |
| Route Handler | HTTP endpoint | Webhooks, public REST | Duplicating every form mutation |
middleware.ts basics
Definition
Place middleware.ts (or proxy.ts in newer Next.js versions) at the project root or src/. Export a middleware function and optional config.matcher to scope which paths run it.
// middleware.ts
import { NextResponse } from "next/server";
import type { NextRequest } from "next/server";
export function middleware(request: NextRequest) {
const { pathname } = request.nextUrl;
if (pathname.startsWith("/dashboard") && !request.cookies.get("session")) {
return NextResponse.redirect(new URL("/login", request.url));
}
return NextResponse.next();
}
export const config = {
matcher: ["/dashboard/:path*", "/settings/:path*"],
};Execution order:
Request → middleware → matched route (page/layout/route.ts) → responseMiddleware runs on every matched request including RSC fetches — keep it fast.
Interview: Middleware is a gate, not a data layer. Redirect unauthenticated users; do not fetch user profiles here.
cookies() and headers()
Definition
In middleware — use request.cookies and request.headers (sync, edge-safe).
In Server Components / Actions — use async cookies() and headers() from next/headers.
// middleware.ts
export function middleware(request: NextRequest) {
const token = request.cookies.get("session")?.value;
const country = request.headers.get("x-vercel-ip-country") ?? "US";
// ...
}// app/dashboard/page.tsx — Server Component
import { cookies, headers } from "next/headers";
export default async function DashboardPage() {
const cookieStore = await cookies();
const session = cookieStore.get("session");
const userAgent = (await headers()).get("user-agent");
return <div>Session: {session?.value ? "active" : "none"}</div>;
}Setting cookies in middleware:
const response = NextResponse.next();
response.cookies.set("locale", "en", {
path: "/",
httpOnly: true,
secure: true,
sameSite: "lax",
maxAge: 60 * 60 * 24 * 365,
});
return response;Interview: Calling
cookies()orheaders()in a Server Component marks that route dynamic. Read session in leaves inside Suspense when using PPR.
redirect() and rewrite()
Definition
redirect() — throws internally; sends a 307/308 to the browser. Use in Server Components, Actions, and Route Handlers.
rewrite() — internal URL mapping; browser URL stays the same. Use for A/B tests, micro-frontends, or proxying.
// app/admin/page.tsx
import { redirect } from "next/navigation";
import { verifySession } from "@/lib/auth";
import { cookies } from "next/headers";
export default async function AdminPage() {
const session = await verifySession((await cookies()).get("session")?.value);
if (!session?.isAdmin) redirect("/login?next=/admin");
return <h1>Admin</h1>;
}Middleware rewrite (user sees /blog, server serves /blog-v2):
// middleware.ts
import { NextResponse } from "next/server";
import type { NextRequest } from "next/server";
export function middleware(request: NextRequest) {
if (request.cookies.get("ab")?.value === "v2") {
return NextResponse.rewrite(new URL("/blog-v2", request.url));
}
return NextResponse.next();
}| API | Visible URL change | Use case |
|---|---|---|
redirect() | Yes | Auth gate, post-login navigation |
NextResponse.redirect() | Yes | Middleware auth redirect |
rewrite() / NextResponse.rewrite() | No | A/B routing, legacy path proxy |
Interview:
redirectchanges what the user sees.rewriteis server-side routing — good for experiments without breaking bookmarks.
Authentication patterns
Definition
Common Next.js auth architectures:
Session cookie (HttpOnly) → middleware checks presence; RSC verifies server-side
JWT in memory + refresh cookie → short-lived access in client; refresh via Route Handler
OAuth (Auth.js / NextAuth) → callback Route Handler sets session cookie
Edge-compatible JWT verify → middleware decodes JWT without DB (watch clock skew)Middleware auth gate (cookie presence only):
// middleware.ts
import { NextResponse } from "next/server";
import type { NextRequest } from "next/server";
const PUBLIC = ["/", "/login", "/signup"];
export function middleware(request: NextRequest) {
const { pathname } = request.nextUrl;
if (PUBLIC.includes(pathname)) return NextResponse.next();
if (!request.cookies.get("session")) {
const login = new URL("/login", request.url);
login.searchParams.set("next", pathname);
return NextResponse.redirect(login);
}
return NextResponse.next();
}Full session verification in Server Component (source of truth):
// lib/auth.ts + app/dashboard/page.tsx
export default async function DashboardPage() {
const session = await getServerSession(); // reads cookie, hits DB or cache
if (!session) redirect("/login");
return <Dashboard user={session.user} />;
}Never pass raw session tokens to Client Components as props — pass derived user data only.
Real-world example
Role-based access in middleware + fine-grained checks in Actions:
// middleware.ts — coarse gate
if (pathname.startsWith("/admin") && !request.cookies.get("session")) {
return NextResponse.redirect(new URL("/login", request.url));
}
// app/admin/actions.ts — fine gate
"use server";
export async function deleteUser(id: string) {
const session = await getServerSession();
if (!session?.roles.includes("admin")) throw new Error("Forbidden");
await db.user.delete({ where: { id } });
}Interview: Middleware = fast coarse gate (cookie exists, route prefix). Server Actions and RSC = authoritative auth + authorization.
Authorization vs authentication
Definition
Authentication → who is this user? (session, JWT, OAuth)
Authorization → can this user do this action? (roles, ABAC, resource ownership)Middleware handles authentication redirects. Authorization belongs in Server Actions, Route Handlers, and data access layers — middleware cannot know resource-level permissions without a DB call.
"use server";
export async function updatePost(postId: string, formData: FormData) {
const session = await getServerSession();
const post = await db.post.findUnique({ where: { id: postId } });
if (!session || post?.authorId !== session.userId) {
return { error: "Forbidden" };
}
// ...
}Interview: “We check auth in middleware” is incomplete — middleware cannot authorize resource-level actions without expensive lookups.
Matcher and edge runtime
Definition
config.matcher limits middleware execution — exclude static assets and _next internals.
export const config = {
matcher: [
"/((?!_next/static|_next/image|favicon.ico|.*\\.(?:svg|png|jpg|jpeg|gif|webp)$).*)",
],
};Middleware defaults to the Edge Runtime — no Node.js APIs (fs, native crypto modules). Use Web Crypto or move logic to Route Handlers if you need Node.
export const config = {
matcher: "/api/:path*",
runtime: "edge", // explicit — default for middleware
};Interview: Matcher regex mistakes cause middleware on every asset request — burns edge budget and adds latency.
Common footguns
Definition
Heavy DB in middleware → slow every request; use cookie/JWT check only
Authorization only in middleware → resource checks need server layer
Setting cookies without flags → missing HttpOnly / Secure / SameSite
Middleware on static files → bad matcher — slows asset delivery
Passing tokens to client → session secret must stay server-sideSenior Interview Summary
Middleware
Edge gate before render. Redirect, rewrite, set cookies, inject headers. Fast and matcher-scoped.
Request APIs
cookies()/headers()in RSC mark routes dynamic. Middleware usesrequest.cookies/request.headers.
Auth layering
Middleware = coarse gate (session exists). RSC/Actions = verify session + authorize resource access.
Best mental model
Middleware → "Before the route — redirect/rewrite/header."
redirect() → "User sees new URL."
rewrite() → "Server serves different path, same URL."
Session → "HttpOnly cookie; verify server-side, never expose raw token to client."Practical rule
Unauthenticated → redirect in middleware
Who is user → getServerSession in RSC/Action
Can user do X → authorize in Action/Route Handler/data layer
A/B routing → rewrite in middleware
Never → DB profile fetch in middleware on every request