Verified against Claude Code · 2026-07-24
Write Next.js middleware that gates routes without slowing down every request
Produces a middleware.ts auth guard scoped with a real matcher config, doing only a light signature-and-expiry check at the edge instead of a full database round trip on every matched request.
The prompt
Ready to copy — highlighted parts are example details you can swap.
You are writing middleware.ts to gate a set of protected routes with a cheap session check, scoped so it only runs where it's actually needed and does no more work than that check requires. PROTECTED ROUTE PATTERNS /dashboard/* and /billing/*, but not /dashboard/status, which is a public health page SESSION CHECK METHOD A signed JWT in a cookie named __session, verified against an edge-compatible HMAC secret REDIRECT DESTINATION /login RUNTIME Edge (the default) — no Node-only dependency needed for the cheap signature check TRAFFIC PROFILE /dashboard/* gets roughly 40,000 requests/day; /billing/* is low-traffic but security-sensitive CONSTRAINTS Middleware runs before any page renders, on every request that matches its scope, and on the edge runtime by default — treat that as a hard latency and API-surface budget, not a place to prove how much validation logic can be packed in. Do only a light, cheap check here: read the session cookie or token and verify its signature and expiry. Never make a database call, an external API call, or anything else with meaningful and variable latency directly inside middleware, because that latency now applies to every single matched navigation on the site, not just the ones that actually needed the deeper check. If deeper validation is genuinely required — confirming a session against a revocation list, loading full role-based permissions — split the work: middleware does the cheap signature-and-expiry check and redirects anything obviously invalid immediately, and the actual page or a layout beneath it does the heavier check, which only runs for requests that already passed the cheap gate and therefore pays that heavier cost far less often than every single request would. Scope the middleware with the matcher config, or an equivalent explicit path check, rather than relying on a manual pathname branch inside the middleware body to skip static assets, image optimization requests, and public routes — an unscoped or loosely-scoped middleware silently adds its check to every asset request too, not just page navigations, a cost that shows up in real latency metrics without ever showing up in a code review of the auth logic itself, since the auth logic in isolation looks perfectly correct. On redirect, preserve the original destination as a callback query parameter so the user returns to where they meant to go after authenticating, rather than always landing on a generic default page regardless of where the request actually originated. If the stated traffic profile includes a route with very high request volume, name the specific cost of any check running there, however cheap it looks in isolation — a check that's trivial at low volume can still be worth optimizing further once it's multiplied across tens of thousands of daily requests. OUTPUT FORMAT The middleware.ts file including its config.matcher, followed by one paragraph naming exactly what this middleware does not check, and exactly where that heavier check actually lives instead — a specific layout or page file, not a vague "later in the request."
Customize
Optional — swap in your own details for the highlighted parts above.
Why this works
Middleware's biggest practical risk isn't getting the auth logic wrong, it's putting the wrong kind of work in it: because middleware runs on every matched request on the edge runtime before any caching or rendering happens, a database call or third-party API check placed there adds that latency to every single navigation that matches the scope, not just the ones that needed deep validation, and that cost is invisible in a code review that only checks whether the auth logic is correct rather than where it physically executes. The explicit light-then-heavy split — a cheap signature and expiry check in middleware, anything expensive pushed to a layout or page that only runs for requests already past the cheap gate — is the concrete fix, and stating it as a rule rather than a suggestion stops the model from reaching for the more "thorough-looking" single-function version that happens to be slow at scale even though it looks more complete on the page. Requiring the matcher config instead of manual pathname checks matters operationally in a way that's easy to miss during development: an unscoped middleware silently runs on _next/static and _next/image requests too, adding its check's latency to every asset load on the protected pages, not just the page navigation itself — a cost that's essentially invisible locally, where asset requests are near-instant, and only shows up as a measurable regression once real network latency and real request volume are involved. Tying the traffic profile into the constraints, rather than treating cost as an abstract concern, is what turns "middleware should be cheap" from a platitude into a number worth checking against — a signature check that costs a fraction of a millisecond is genuinely free at low volume, but the same check run 40,000 times a day against a route that could have been scoped more narrowly is a real, summable cost, and naming the actual traffic figure is what lets that tradeoff get evaluated concretely instead of asserted in the abstract.
What you get back
config.matcher: ['/dashboard/:path*', '/billing/:path*']. Middleware verifies the __session JWT's signature and expiry only, and redirects to /login?callbackUrl=<original path> if either check fails. It does NOT check whether the session's role still has billing access to a specific sub-page — that permission check happens in app/billing/layout.tsx, which does have database access to load full role data and only runs for requests middleware already let through, so it pays that cost far less often than every request to /billing/* would.
Verified against
Claude Code Sonnet 4.6 · 2026-07-24
Cursor Cursor 2.1 · 2026-08-02
Changelog
- 2026-07-24 — Initial publish, verified against Claude Code (Sonnet 4.6) and Cursor 2.1 on Next.js 16 edge middleware.
Need this built into your business?
If a prompt isn't enough — custom software, built and maintained for you — that's Scult's day job.
EXPLORE CUSTOM SOFTWARE
