Topics
v5.0.0
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.

LayerRuns whenBest forAvoid
MiddlewareBefore route match/renderAuth redirect, locale, bot blockingDB queries, business logic
Server ComponentDuring RSC renderSession read, data fetchClient-only APIs
Server ActionPOST mutationValidate session, mutateLong-running jobs
Route HandlerHTTP endpointWebhooks, public RESTDuplicating 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) → response

Middleware 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() or headers() 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(); }
APIVisible URL changeUse case
redirect()YesAuth gate, post-login navigation
NextResponse.redirect()YesMiddleware auth redirect
rewrite() / NextResponse.rewrite()NoA/B routing, legacy path proxy

Interview: redirect changes what the user sees. rewrite is 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-side

Senior 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 uses request.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

Last updated on