Use when designing or reviewing Next.js middleware (`middleware.ts`): cross-cutting request/response transforms before route resolution, Edge Runtime constraints, `matcher` config, `NextRequest`/`NextResponse` API, the four response shapes (next/rewrite/redirect/direct), canonical patterns (auth gate, locale routing, A/B testing, header injection, geo-routing, bot blocking), and the design rule that middleware is for cross-cutting concerns across many routes — never per-route business logic. Do NOT use for per-route HTTP endpoint logic (use route-handler-design), Server Action mutations (use server-actions-design), abstract HTTP semantics (use http-semantics), CSP and hardening (use security-fundamentals), or the cross-cutting streaming model (use streaming-architecture).
Installer avec Codex ou Claude Copiez ce prompt, collez-le dans Codex, Claude ou un autre assistant, puis laissez-le vérifier la page du skill et l'installer pour vous.
Une commande directe contourne le prompt de vérification. Examinez la source avant de l'exécuter.
Use when designing or reviewing Next.js middleware (`middleware.ts`): cross-cutting request/response transforms before route resolution, Edge Runtime constraints, `matcher` config, `NextRequest`/`NextResponse` API, the four response shapes (next/rewrite/redirect/direct), canonical patterns (auth gate, locale routing, A/B testing, header injection, geo-routing, bot blocking), and the design rule that middleware is for cross-cutting concerns across many routes — never per-route business logic. Do NOT use for per-route HTTP endpoint logic (use route-handler-design), Server Action mutations (use server-actions-design), abstract HTTP semantics (use http-semantics), CSP and hardening (use security-fundamentals), or the cross-cutting streaming model (use streaming-architecture).
license
MIT
allowed-tools
Read Grep
metadata
{"metadata":"{\"schema_version\":6,\"version\":\"1.0.0\",\"type\":\"capability\",\"category\":\"engineering\",\"domain\":\"engineering/frontend\",\"scope\":\"reference\",\"owner\":\"skill-graph-maintainer\",\"freshness\":\"2026-05-17\",\"drift_check\":\"{\\\\\\\"last_verified\\\\\\\":\\\\\\\"2026-05-17\\\\\\\"}\",\"eval_artifacts\":\"planned\",\"eval_state\":\"unverified\",\"routing_eval\":\"absent\",\"comprehension_state\":\"present\",\"stability\":\"experimental\",\"keywords\":\"[\\\\\\\"Next.js middleware\\\\\\\",\\\\\\\"middleware.ts file\\\\\\\",\\\\\\\"NextRequest NextResponse\\\\\\\",\\\\\\\"matcher config middleware\\\\\\\",\\\\\\\"Edge Runtime constraints\\\\\\\",\\\\\\\"NextResponse.redirect rewrite next\\\\\\\",\\\\\\\"auth check before route\\\\\\\",\\\\\\\"locale routing i18n middleware\\\\\\\",\\\\\\\"A/B testing variant rewrite\\\\\\\",\\\\\\\"CSP nonce middleware\\\\\\\",\\\\\\\"geo-routing X-Vercel-IP-Country\\\\\\\",\\\\\\\"request header injection\\\\\\\",\\\\\\\"bot blocking middleware\\\\\\\",\\\\\\\"middleware cookie set\\\\\\\"]\",\"triggers\":\"[\\\\\\\"how do I redirect unauthenticated users to login in Next.js\\\\\\\",\\\\\\\"how do I run code before every request in Next.js\\\\\\\",\\\\\\\"how do I set security headers globally in Next.js\\\\\\\",\\\\\\\"how do I do locale routing in App Router\\\\\\\",\\\\\\\"how do I do an A/B test with rewrites\\\\\\\",\\\\\\\"why does my middleware run on static assets\\\\\\\",\\\\\\\"can middleware do a database query\\\\\\\",\\\\\\\"how do I generate a CSP nonce per request\\\\\\\"]\",\"examples\":\"[\\\\\\\"design middleware that redirects unauthenticated users to /login while letting public routes through, configured via a matcher\\\\\\\",\\\\\\\"add a middleware that generates a per-request CSP nonce and injects it into both the request and response headers\\\\\\\",\\\\\\\"implement locale routing that detects Accept-Language and rewrites /about to /en/about for new visitors\\\\\\\",\\\\\\\"add bot blocking that returns 403 for known scraper user-agents while letting search-engine bots through\\\\\\\",\\\\\\\"tune a middleware that runs on every request down to 5ms so it stops adding latency to image fetches\\\\\\\"]\",\"anti_examples\":\"[\\\\\\\"implement a /api/posts POST endpoint (use route-handler-design)\\\\\\\",\\\\\\\"implement a delete-comment mutation triggered from a form button (use server-actions-design)\\\\\\\",\\\\\\\"explain what an HTTP 308 means vs 307 (use http-semantics)\\\\\\\",\\\\\\\"design the full CSP policy and the rest of the security-header strategy (use security-fundamentals)\\\\\\\",\\\\\\\"design a long-lived SSE stream from middleware (use streaming-architecture)\\\\\\\",\\\\\\\"design the CSP policy, threat model, or OWASP audit for a system (use security-fundamentals)\\\\\\\",\\\\\\\"decide what an HTTP method, status code, or header should mean per RFC 9110 (use http-semantics)\\\\\\\",\\\\\\\"design signature verification, idempotency, or retry semantics for vendor webhooks (use webhook-integration)\\\\\\\"]\",\"relations\":\"{\\\\\\\"related\\\\\\\":[\\\\\\\"route-handler-design\\\\\\\",\\\\\\\"server-actions-design\\\\\\\",\\\\\\\"http-semantics\\\\\\\",\\\\\\\"security-fundamentals\\\\\\\",\\\\\\\"server-components-design\\\\\\\",\\\\\\\"client-server-boundary\\\\\\\",\\\\\\\"webhook-integration\\\\\\\"],\\\\\\\"boundary\\\\\\\":[{\\\\\\\"skill\\\\\\\":\\\\\\\"route-handler-design\\\\\\\",\\\\\\\"reason\\\\\\\":\\\\\\\"middleware runs once before route resolution and applies to many routes via a matcher; route-handler-design runs for one route and one method after route resolution. Middleware owns the cross-cutting layer (auth gate, locale rewrite, header injection); route handlers own the per-route logic. They compose: middleware passes through to the handler, the handler executes, the response flows back.\\\\\\\"},{\\\\\\\"skill\\\\\\\":\\\\\\\"server-actions-design\\\\\\\",\\\\\\\"reason\\\\\\\":\\\\\\\"server-actions-design owns the internal-mutation surface invoked from the app's own UI; middleware is the cross-cutting request preprocessor that runs before any route or action. A Server Action call passes through middleware on its way to the server.\\\\\\\"},{\\\\\\\"skill\\\\\\\":\\\\\\\"server-components-design\\\\\\\",\\\\\\\"reason\\\\\\\":\\\\\\\"server-components-design owns the render path that produces a page; middleware runs upstream of render and can rewrite, redirect, or pass through. Middleware does not replace render; it gates and rewrites it.\\\\\\\"}],\\\\\\\"verify_with\\\\\\\":[\\\\\\\"code-review\\\\\\\",\\\\\\\"security-fundamentals\\\\\\\"]}\",\"mental_mo…","skill_graph_source_repo":"https://github.com/jacob-balslev/skill-graph","skill_graph_protocol":"Skill Metadata Protocol v4","skill_graph_project":"Skill Graph","skill_graph_canonical_skill":"skills/middleware-patterns/SKILL.md","skill_graph_export_description":"shortened for Agent Skills 1024-character description limit; canonical source keeps the full routing contract","skill_graph_canonical_description_length":"1178"}
Middleware Patterns
Coverage
The discipline of designing Next.js middleware: the one-file-per-project contract (middleware.ts at the root or under src/, single default export), the Edge Runtime constraints that govern what code can and cannot run there, the matcher config that filters which paths trigger middleware, the NextRequest / NextResponse API surface (cookies, geo, IP, headers), the four response shapes (next, rewrite, redirect, direct response), the canonical pattern library (authentication gate, locale routing, A/B testing, security header injection, geo-routing, bot blocking, request-id correlation), the performance discipline that every matched request pays the cost, and the central design rule: middleware is for cross-cutting concerns that apply across many routes — never for per-route business logic.
Philosophy
The Pages Router's request lifecycle was: server hits getServerSideProps, which returns props, which render the page. The App Router added more layers (Server Components, Server Actions, Route Handlers), but kept one thing constant — they all run after the route is resolved.
Middleware runs before. It is the only layer where you can intercept a request without knowing which route it will eventually hit. That makes it the right home for concerns that apply across the entire app or large subsets of it: "every request needs an auth check", "every request needs a request-id header", "every request to /admin/* needs a role check", "every page needs a CSP nonce".
The architectural trade is breadth for power. Middleware:
Runs on the Edge Runtime — limited APIs, no Node-specific dependencies, no large packages.
Runs on every matched request — performance ceiling matters because the cost multiplies.
Has no per-route knowledge until the rewrite/redirect resolves — cannot read route-specific params or query the database for that route's data.
Cannot read the response body — it sits in front of the response, not over it.
In exchange, it can shape the entire request/response edge in a single place. Done well, it removes ceremony from every route; done badly, it adds latency to every request and concentrates business logic in a file that's hard to test.
The discipline of middleware is to keep it small, fast, and cross-cutting. When a piece of logic only applies to one route, it does not belong here. When it requires a database lookup that adds 50ms, it does not belong here. When the code is hard to reason about, it definitely does not belong here.
The File Contract
// middleware.ts (project root, or src/ if using src layout)import { NextResponse } from'next/server'importtype { NextRequest } from'next/server'exportasyncfunctionmiddleware(request: NextRequest) {
// ... transform or gate the request ...returnNextResponse.next()
}
exportconst config = {
matcher: ['/((?!api|_next/static|_next/image|favicon.ico).*)'],
}
One file per project. There is no chain of middleware files. Compose multiple concerns inside a single middleware function.
Default export. The named export pattern (export async function middleware) and the default export both work; pick one.
config.matcher filters which paths trigger middleware. Critical: the default — no matcher — runs on every single request including static assets. Always set a matcher.
Matchers compile to regular expressions at build time. They cannot use runtime values. Complex negative lookaheads are common because the default-matches-everything behavior is rarely what you want — image fetches, static assets, prefetch requests, and webhook routes should usually be excluded.
The biggest middleware footgun is forgetting to exclude /_next/static and /_next/image, which makes every image fetch run middleware code on the hot path.
The Four Response Shapes
import { NextResponse } from'next/server'// 1. Pass through — let the request continue to its routereturnNextResponse.next()
// 2. Rewrite — internally route to a different path; URL bar unchangedreturnNextResponse.rewrite(newURL('/en/about', request.url))
// 3. Redirect — send a 30x to the browser; URL bar changesreturnNextResponse.redirect(newURL('/login', request.url))
// 4. Direct response — short-circuit; return a response without hitting any routereturnnewNextResponse('Forbidden', { status: 403 })
Choose based on what the user should see and what should change:
Goal
Use
Continue to the originally requested route, possibly with modified headers/cookies
next() (often with .headers.set() on the response)
Serve a different route's content under the same URL (A/B test, locale variant, feature flag)
rewrite
Send the user to a different URL (login redirect, canonical redirect, locale-detection redirect)
redirect
Block the request entirely (rate limit hit, bot blocked, missing auth on protected API)
direct response with appropriate status
Rewrite vs redirect is a load-bearing distinction: rewrites are invisible to the user (the URL stays the same), redirects are visible (the URL changes and the browser does a second request). If you want the user to see they've been moved (/old-path → /new-path), redirect. If you want to keep their URL and serve different content (A/B variant, internal locale path), rewrite.
Edge Runtime Constraints
Middleware runs on Edge. Things to know:
Capability
Available
fetch
✅
Web Crypto (crypto.subtle, crypto.randomUUID)
✅
Web Streams (ReadableStream, TransformStream)
✅
URL, URLSearchParams, Request, Response
✅
setTimeout / setInterval
⚠️ best-effort, may not fire after response
Node crypto module
❌
Node fs, child_process, net, dns
❌
Most npm packages that aren't pure JS
❌
Large bundle sizes
❌ (Vercel: ~1MB ceiling on middleware code)
Middleware code is bundled and shipped to Edge nodes globally. Cold-start is fast (~10–50ms) but the trade is a tight capability surface. Anything you import — including transitive dependencies — must be Edge-compatible. A single import of a Node-only package breaks the build.
Practical consequence: don't reach for ORMs, full SDKs, or complex libraries in middleware. Hand-roll the small piece you need (decode a JWT, hash a token, parse a cookie). If the work genuinely needs Node — verifying a webhook signature with a vendor SDK, hitting a database — push it down into a Route Handler or Server Action instead.
The session cookie is checked, not verified. Cryptographic verification belongs inside the routes — middleware is for fast cookie presence checks. A signed-cookie verification that requires a JWT library is fine if the library is Edge-compatible; a database lookup to validate the session is not — that 50ms hits every protected page load.
A first-visit user lands on /about, gets redirected to /en/about. Subsequent visits to locale-prefixed paths pass through. The redirect-once pattern keeps the URL canonical and lets the rest of the app assume locale is in the path.
The user sees /pricing in their URL bar but receives /pricing-a or /pricing-b. The cookie pins their variant so subsequent visits are consistent. The rewrite preserves the canonical URL for analytics and sharing.
The nonce flows to the request headers (so Server Components can read it via headers() and inject it into <script> tags) and to the response headers (so the browser enforces the CSP). The policy itself is just an example; the actual rules belong to the broader security strategy in security-fundamentals.
UA strings are trivially spoofable — bot blocking via user-agent works for honest crawlers but does not stop adversaries. Use this for noise reduction, not security.
The request-id flows through to the route (readable via headers()) and back out to the client (visible in DevTools). Pair with structured logging that includes the id, and you get end-to-end traceability.
Composing Multiple Concerns
There is one middleware.ts. Combine concerns inside it — typically in a clear order:
exportasyncfunctionmiddleware(request: NextRequest) {
// 1. Block bots first — fast rejectconst ua = request.headers.get('user-agent') ?? ''if (BLOCKED_AGENTS.some((re) => re.test(ua))) {
returnnewNextResponse('Forbidden', { status: 403 })
}
// 2. Locale detection — redirect once for first-visit usersconst localeRedirect = applyLocaleRouting(request)
if (localeRedirect) return localeRedirect
// 3. Auth gate — redirect to login for protected routesconst authRedirect = applyAuthGate(request)
if (authRedirect) return authRedirect
// 4. Pass through with security headers + request-idreturnapplyHeaders(request)
}
Each helper returns either a short-circuit NextResponse or null (continue). The shape is a small chain of guards; the file stays readable. When the chain grows past ~5 concerns, it's a signal that the middleware is doing too much — push something down to per-route logic or to a separate request-time hook.
Performance Discipline
Every matched request pays the middleware cost. Three rules:
Tune the matcher. If only /dashboard/* needs auth, match only /dashboard/*. Don't run auth checks against image fetches.
Cap the time budget. Target <10ms p99 for middleware execution. Slow middleware degrades every page on the site.
No database calls. A network round-trip in middleware is a tax on every request. Cache aggressively; use signed cookies that carry the data middleware needs without a lookup; defer DB checks to the route.
The performance budget is invisible until you load-test the site and see middleware dominate the latency profile. Build it in from the start.
Common Anti-Patterns
Anti-pattern
Why it's wrong
Fix
No matcher — middleware runs on /_next/static, /_next/image, /favicon.ico
Adds latency to every image fetch and static asset
Set a matcher with negative lookahead excluding _next paths and assets
Database query in middleware
Network round-trip on every request
Use signed cookies that carry the data, or push the lookup down to the route
Putting per-route business logic in middleware
Centralized file that hides the logic from the route that owns it
Move to the route; keep middleware for genuine cross-cutting
Importing a Node-only package
Edge build fails
Use Edge-compatible alternatives, or move the work to a Route Handler
Verifying a JWT signature against a remote JWKS endpoint without caching
Network call per request
Cache the JWKS in memory; or defer verification to the route
redirect when rewrite was meant (or vice versa)
URL changes when it shouldn't, or stays the same when it should
Choose based on whether the user should see the URL change
Forgetting to copy headers to the response when modifying request headers
Request-side changes invisible to client
Use NextResponse.next({ request: { headers: ... } }) AND set the same on response.headers if the client needs to see them
Setting cookies on the request — middleware can't modify the request cookies the client sees
Cookie set silently lost
Set cookies on the response via response.cookies.set(...)
Running middleware on webhook routes that need raw body access
Middleware can consume the body or otherwise interfere with HMAC verification
Exclude webhook paths from the matcher
Single 100-line middleware doing 8 different things
Untestable, slow, hard to reason about
Decompose into named helpers; consider whether some concerns belong per-route
Verification
After applying this skill, verify:
config.matcher is set and excludes _next/static, _next/image, favicon.ico, and any other paths that don't need middleware (typically webhooks).
No database queries or other I/O that adds >10ms to the request happen inside middleware.
All imported packages are Edge-Runtime-compatible (no Node crypto, fs, child_process, net).
Cookies that need to reach the client are set on the response (response.cookies.set), not on the request.
Modified request headers use NextResponse.next({ request: { headers } }) so they flow to the route.
The choice between rewrite and redirect matches whether the URL should visibly change.
Webhook routes are excluded from the matcher to preserve raw-body access.
Multiple concerns are decomposed into named helpers; one concern per helper.
Auth checks are fast cookie/signature checks, not database lookups — deeper verification is deferred to the route.
Security-header injection (if used) coordinates with the broader security strategy defined in security-fundamentals.
Grounding Sources
Next.js docs — Middleware. The canonical reference for the middleware.ts convention.
Next.js docs — Matcher config. The path-filtering rules.