Next.js lets you declare redirects(), rewrites() and headers() in next.config, or implement the same behaviour in middleware. How do you choose between them?
answer
- build-time config vs per-request code
- matching versus computing
- has and missing cover conditionals
- a nonce cannot be static
- declarative first, code when forced
basics
~20 sUse next.config when the rule is static and known at build time — it is declarative routing config with no JavaScript running per request. Use middleware when the decision needs per-request logic the config cannot express, such as a computed value, a lookup, or a generated nonce.
solid answer
~50 s`next.config` exposes async `redirects()`, `rewrites()` and `headers()` that return arrays of `{ source, destination }` or `{ source, headers }` rules. They are evaluated once at build time into routing configuration, so no function of yours runs per request — cheap, predictable, and easy to audit in one file. They are not purely static, though: each entry accepts `has` and `missing` conditions that match on a header, cookie, host or query param, so "redirect when this cookie exists" needs no middleware. Reach for middleware when the decision needs something config cannot express — a value you compute, a cookie you must parse or verify rather than merely match, a per-request `Content-Security-Policy` nonce, or a destination derived at runtime. The practical rule: express it declaratively if you can, because middleware runs code on every matched request and becomes a place bugs hide.
go deeper
Know that next.config has redirects(), rewrites() and headers() returning source/destination rules, and that middleware is the code-based alternative for the same three outcomes.
Explain that the config functions run at build time into routing rules while middleware runs per request, and that has/missing conditions cover many conditional cases without code.
Weigh the operational side: middleware sits on the latency path of every matched request and is a single point of failure, so move any rule that a static pattern can express down into the config.
Own the URL and header policy as a whole — decide what belongs in an auditable declarative table versus runtime code, who may add to each, and how a redirect map stays reviewable as it grows past a few dozen entries.
## Two places, similar-looking outcomes Next gives you the same three outcomes at two levels. In `next.config.ts`: ```ts export default { async redirects() { return [{ source: '/old-blog/:slug', destination: '/blog/:slug', permanent: true }] }, async rewrites() { return [{ source: '/docs/:path*', destination: '/documentation/:path*' }] }, async headers() { return [{ source: '/(.*)', headers: [{ key: 'X-Frame-Options', value: 'DENY' }] }] }, } ``` In `middleware.ts`, the same three outcomes are `NextResponse.redirect()`, `NextResponse.rewrite()` and `response.headers.set()`. ## What the config form actually is Those functions are `async` and run at build time (and on server start), not per request. What they return is turned into routing rules the server evaluates. So: - No JavaScript of yours executes per request. There is no cold start, no CPU budget, no bundle for these. - Everything lives in one file, so an auditor can read the site's whole redirect map at once. - `source` and `destination` use path-to-regexp-style patterns — `:slug` for a segment, `:path*` for the rest, and `(.*)` for regex-ish matching — so parameters propagate without code. - `permanent: true` emits 308, `permanent: false` emits 307. - Config `rewrites()` may target an external URL, which turns Next into a proxy for that path — genuinely useful for putting a legacy app or a separate service under one hostname. - `rewrites()` may return the object form `{ beforeFiles, afterFiles, fallback }` to control where in the resolution order the rules apply relative to your own routes. ## They are not as static as people think The most common wrong reason to reach for middleware is "my redirect is conditional". Config entries take `has` and `missing` arrays that match on a `header`, `cookie`, `host` or `query`, by presence or by value pattern: ```ts { source: '/', has: [{ type: 'cookie', key: 'onboarded', value: 'true' }], destination: '/dashboard', permanent: false, } ``` That covers a large slice of what teams write middleware for, at zero per-request cost. ## When you genuinely need middleware The config form matches; it does not compute. Middleware earns its place when the decision requires running your code: - **A value must be derived.** Bucketing a user into an A/B variant, hashing something, picking a tenant from a lookup. - **A credential must be inspected, not merely matched.** Config can see that a cookie exists; it cannot verify a signature or check an expiry. - **A per-request value must be generated.** The clearest example is a `Content-Security-Policy` nonce: it must be unique per response, so it cannot live in a static `headers()` entry. Middleware generates it, sets the CSP response header, and forwards the nonce inward on a request header for the page to use. - **The destination is computed at runtime**, not drawn from a fixed table. - **You need to attach data to the inbound request**, which only `NextResponse.next({ request })` can do. ## Costs and failure modes Config rules cost essentially nothing per request, but they are frozen at build time — changing one is a deploy. A very large hand-maintained redirect table also becomes hard to reason about, and order matters. Middleware costs real work on every request it handles, which puts it on the latency path of your whole site. It is also the easiest place to create an outage: a thrown error or a redirect loop in middleware affects everything, not one route. And because it is code, it drifts — the rule that was obvious when written becomes an unexplained branch a year later. ## The decision, in one line Can a static pattern plus `has`/`missing` express this rule? Then put it in `next.config`. Does the decision need a value you compute, verify or generate? Then it belongs in middleware. When both work, prefer the declarative one: it is faster, auditable in a single file, and cannot throw. Mixing the two is normal. A typical app keeps its long-lived URL map and its blanket security headers in `next.config`, and uses middleware only for the handful of decisions that truly need runtime logic.
- Can a redirect declared in next.config be conditional at all?Yes. Each entry accepts `has` and `missing` arrays matching on `header`, `cookie`, `host` or `query` — presence or value pattern. That handles a lot of what teams write middleware for, with no per-request code. What it cannot do is verify a value, such as checking a token's signature or expiry.
- Where would you put a Content-Security-Policy header, and does that answer change if it needs a nonce?A fixed CSP belongs in `headers()` in `next.config` — one place, no runtime cost. A nonce-based policy cannot: the nonce must be unique per response, so middleware generates it, sets the CSP response header, and forwards the nonce inward on a request header for the page to consume.
- What does permanent: true mean on a next.config redirect entry?Next emits a 308 for that rule, and `permanent: false` emits 307. It is a declarative equivalent of passing the status as the second argument to `NextResponse.redirect`. Choose it when the move really is permanent, because clients and caches are allowed to remember a permanent redirect for a long time.
saying these in an interview costs you the question
- Says next.config redirects cannot be conditional at all
- Puts every redirect in middleware because it is 'more flexible'
- Thinks redirects() runs on each request because it is async
- Serves a static CSP nonce from next.config headers()
- Ignores that middleware failures affect every matched route