Rewrites map an incoming request path to a different destination while leaving the URL in the browser untouched. When the destination is an absolute URL, the rule becomes a proxy: your app forwards the request to another service and returns its response under your own origin.
Rewrites to an external API are the common case. The browser talks only to your domain, so the request is same-origin and the CORS problem disappears.
// next.config.ts
import type { NextConfig } from 'next'
const nextConfig: NextConfig = {
async rewrites() {
return [
{ source: '/api/catalog/:path*', destination: 'https://catalog.example.com/v2/:path*' },
]
},
}
export default nextConfigA browser request to /api/catalog/items reaches the catalog service at /v2/items and comes back as if your own server produced it. The address bar, the network panel, and any cookie scoping all see one origin.
What a rewrite does and does not give you
The value of proxying through your own domain is mostly about the browser. Same-origin requests skip preflight negotiation, and first-party cookies apply without cross-site restrictions.
What you do not get is secrecy. The rule forwards the request as it arrives, so it cannot attach an API key or strip a header, and the upstream hostname is not hidden from anyone reading your config or observing traffic patterns.
If the upstream service needs an API key, a rewrite cannot supply it. Use a Route Handler that reads the key from a server environment variable and calls the service itself, so the secret never has to reach the browser.
The other thing a rewrite does not do is validate. Every request that matches the source is forwarded, including malformed ones, which is fine for a read-only public API and unsuitable for anything that mutates data.
The three rewrite phases
Returning an array from the rewrites function places every rule in one default position: after filesystem routes, before dynamic routes. Returning an object instead lets you choose the phase per rule.
// next.config.ts
async rewrites() {
return {
beforeFiles: [{ source: '/preview/:path*', destination: '/draft/:path*' }],
afterFiles: [{ source: '/api/catalog/:path*', destination: 'https://catalog.example.com/v2/:path*' }],
fallback: [{ source: '/:path*', destination: 'https://legacy.example.com/:path*' }],
}
}Each phase answers a different question. Choosing the wrong one is the most common reason a rule appears to be ignored.
| Phase | Checked | Use it to |
|---|---|---|
| beforeFiles | Before static files and pages | Override a route that already exists |
| afterFiles | After pages, before dynamic routes | Add a path your app does not serve |
| fallback | After dynamic routes, before the 404 | Hand unmatched paths to an older site |
The fallback phase is what makes an incremental migration workable. Anything your Next.js app can serve is served locally, and everything else goes to the legacy system, so you move routes over one at a time without touching the configuration again.
How paths and queries are forwarded
Path parameters use the same syntax as config redirects, and a captured segment is substituted into the destination by name. The star modifier is what carries nested paths through.
Query handling has one rule worth remembering. When the destination does not use a captured parameter, Next.js appends the parameters to the query automatically, and when the destination does use one, it stops doing that.
// next.config.ts
async rewrites() {
return [
{ source: '/legacy/:section/:id', destination: '/archive/:section?item=:id' },
]
}A request to /legacy/press/42 is served by /archive/press with an item query value of 42. Because a parameter already appears in the destination, the second one had to be placed in the query by hand.
Query values that arrive on the original request are always forwarded to the destination, so tracking parameters and filters survive the rewrite without configuration.
Trailing slashes and base paths
Two config options change what your source patterns need to look like, and both cause confusing mismatches when they are set after the rules are written.
If the app uses a trailing slash convention, the source pattern needs one too, and the destination needs one whenever the upstream service expects it. A rule written without slashes simply stops matching once the option is enabled.
A configured base path is prefixed onto both the source and the destination automatically. That is usually right for internal rewrites and wrong for an external destination, so opt an external rule out of the base path explicitly.
The trailing slash behavior article covers how the option changes canonical URLs. For rewrites, the practical rule is to set the option first and write the patterns second.
Keep the destination a constant
A rewrite destination is trusted by your server. If any part of the hostname comes from the incoming request, an attacker can point your server at a host of their choosing, which is server-side request forgery.
Next.js addressed a case where a destination hostname built from request-controlled input could be redirected to an arbitrary host regardless of the rule's hostname suffix. The fixes shipped in 16.2.11 and 15.5.21, and in 16.3.0. Upgrade, and keep external destinations hardcoded regardless of version.
The safe shape is the one used throughout this article. The hostname is a literal in the config file, and only the path is built from the incoming request.
When the target genuinely varies, resolve it on the server from a fixed allow-list inside a Route Handler rather than in a rewrite rule. That gives you a place to reject anything not on the list before a request goes out.
When to use a Route Handler instead
A rewrite is declarative and cheap, which is exactly why it runs out of room quickly. The moment the proxy needs to think, it should be code.
Reach for a Route Handler when the request needs any of the following:
- An API key, signed header, or other credential added on the server.
- Authentication or authorization before the upstream call.
- Input validation, response reshaping, or error translation.
- Rate limiting or logging tied to the caller.
A handler also gives you a place to return a useful status when the upstream service fails, instead of passing a raw error body to the browser. Securing Server Actions and handlers covers the checks that belong on that boundary.
Verifying a rewrite
Because a rewrite produces a normal 200 response, a status code check will not tell you whether the rule matched. Compare the response body instead.
curl -s http://localhost:3000/api/catalog/items | head -c 200If the rule matched, the output is the upstream service's payload rather than your app's 404 page. Requesting the destination directly is a useful second check, since identical bodies confirm the mapping.
Rewrite rules are read when the config file loads, not on every request. Editing the config in development makes Next.js restart the server itself, so wait for the restart notice below before you retest a changed rule.
⚠ Found a change in next.config.ts. Restarting the server to apply the changes...A rule that stubbornly does nothing is often a rule the running server has not read yet.
Check a client-side navigation as well as a direct request. Clicking a link to the rewritten path should produce the same content as loading it fresh, and a difference between the two usually means the rule sits in the wrong phase.
Rune AI
Key Insights
- A rewrite serves content from the destination while the requested URL stays in the address bar.
- An absolute destination proxies an external service through your own origin, which removes the CORS problem.
- The three phases, beforeFiles, afterFiles, and fallback, decide where a rule sits relative to filesystem and dynamic routes.
- Path parameters are forwarded in the query only when the destination does not already use them.
- Never build a destination hostname from request input, because that turns a rewrite into a server-side request forgery hole.
Frequently Asked Questions
Does a rewrite hide my API key?
What is the difference between beforeFiles and afterFiles?
Are path parameters passed to the destination as query values?
Do rewrites work during client-side navigation?
Conclusion
Rewrites map a path in your app to another route or to an external service without changing the URL the browser shows. Pick the phase that matches your intent, keep the destination hostname a constant, and move to a Route Handler as soon as the upstream call needs credentials or validation.
More in this topic
`generateMetadata` Explained with Real Examples
What generateMetadata does, when it runs, and how to use it for real routes: awaited params, deduplicated data fetching, extending parent metadata, and returning a 404 from metadata.
Canonical URLs in Next.js: `metadataBase`, `alternates.canonical`, and Dynamic Pages
How canonical URLs work in the Next.js App Router: setting metadataBase once, writing alternates.canonical per route, handling dynamic segments, and what happens when the base URL is missing.
Open Graph and Twitter Card Metadata in Next.js
How to write Open Graph and Twitter card metadata in the Next.js App Router: the openGraph and twitter fields, automatic card defaults, article tags, and image merge rules.