Configuring `rewrites` and Proxying to an External API

How to map a path in your Next.js app to another route or to an external service with rewrites, including the three rewrite phases, query behavior, and the destination rule that prevents SSRF.

9 min read

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.

typescripttypescript
// 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 nextConfig

A 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.

A rewrite is not a credential store

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.

typescripttypescript
// 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.

PhaseCheckedUse it to
beforeFilesBefore static files and pagesOverride a route that already exists
afterFilesAfter pages, before dynamic routesAdd a path your app does not serve
fallbackAfter dynamic routes, before the 404Hand 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.

typescripttypescript
// 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.

Patched in July 2026

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.

bashbash
curl -s http://localhost:3000/api/catalog/items | head -c 200

If 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.

texttext
 ⚠ 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

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.
RunePowered by Rune AI

Frequently Asked Questions

Does a rewrite hide my API key?

No. A rewrite forwards the request as it arrives and cannot attach a secret header, so any credential still has to come from the browser. When the upstream call needs a key, use a Route Handler that reads the key from a server environment variable.

What is the difference between beforeFiles and afterFiles?

beforeFiles rules are checked before static files and pages, so they can override an existing route. afterFiles rules are checked after those and before dynamic routes, which makes them the safe default for adding new paths.

Are path parameters passed to the destination as query values?

Only when the destination does not use them. If a parameter appears in the destination path, Next.js stops adding the parameters to the query automatically, and you can still add any you need explicitly.

Do rewrites work during client-side navigation?

Yes. Config rewrites are applied to client-side routing as well, so a Link pointing at a rewritten path resolves to the destination content while the displayed URL stays unchanged.

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.