Configuring `redirects` in `next.config.ts`

How to declare redirects in a typed Next.js config file, including permanent versus temporary status codes, path parameters, conditional matching, and how to verify the result.

8 min read

Declaring redirects in next.config.ts is how you map old paths to new ones without writing any request-handling code. The function returns an array of rules, and Next.js applies them on the server before a route is rendered.

Each rule needs three fields: the incoming path pattern, the destination, and whether the move is permanent. The typed config file gives you completion and checking on all of them.

typescripttypescript
// next.config.ts
import type { NextConfig } from 'next'
const nextConfig: NextConfig = {
  async redirects() {
    return [
      { source: '/team', destination: '/about', permanent: true },
    ]
  },
}
export default nextConfig

A request to /team now answers with a 308 status and a location header pointing at /about. The browser follows it, the address bar shows the new path, and nothing renders at the old one.

Permanent, temporary, and the status codes behind them

The permanent flag is the only field that changes the status code. Setting it to true sends a 308, and setting it to false sends a 307.

Next.js deliberately avoids the older 301 and 302 pair. Both are widely mishandled: many clients turn the redirected request into a GET even when the original was a POST, which quietly breaks form submissions and API calls.

Flag valueStatusCached by clientsUse it when
true308Yes, often indefinitelyThe old URL is retired for good
false307NoThe move is temporary or still being tested

Choose false while a change is in flight. A 308 that reaches real browsers can be cached long after you remove the rule, so an incorrect permanent redirect is much harder to walk back than an incorrect temporary one.

For the rare client that cannot handle either code, a rule can carry an explicit status code instead of the permanent flag. The two fields are mutually exclusive, so use one or the other.

Matching paths with parameters

Hardcoding one rule per URL does not scale past a handful. Path patterns use the same syntax as proxy matchers, so a named parameter can capture a segment and reuse it in the destination.

typescripttypescript
// next.config.ts
async redirects() {
  return [
    { source: '/blog/:slug', destination: '/news/:slug', permanent: true },
  ]
}

A request to /blog/hiring redirects to /news/hiring, and the same rule covers every post in the section. The captured value is substituted into the destination by name.

A bare parameter matches exactly one segment. Add a star to match zero or more, which is what you want when the old section had nested paths of unpredictable depth.

Keep the slash before the colon

Writing a pattern without the forward slash in front of a parameter makes Next.js treat the text as a literal string. The rule then matches the destination as well as the source, which is one of the fastest ways to build an infinite redirect loop.

Patterns are anchored to the start of the path, so a rule for /blog never matches /archive/blog. When you need a real expression, wrap one in parentheses after a parameter to constrain what it accepts, such as digits only for a numeric post id.

Conditional redirects with request data

A rule can require more than a path match. The has array adds conditions that must all be satisfied, and the missing array adds conditions that must all fail, before the redirect applies.

typescripttypescript
// next.config.ts
async redirects() {
  return [
    {
      source: '/checkout/:path*',
      missing: [{ type: 'cookie', key: 'cart' }],
      destination: '/cart/empty',
      permanent: false,
    },
  ]
}

Someone opening a checkout URL without a cart cookie is sent to an empty-cart page, while anyone carrying the cookie proceeds normally. Both conditions are evaluated against the incoming request on the server.

Each condition takes a type of header, cookie, host, or query, a key, and an optional value. Leaving the value off means any value matches, which is the right choice when presence is what you care about.

The value can also be a regular expression with a named capture group, and that captured text becomes available in the destination. That covers cases like routing a request to a locale path based on part of a header value.

Where these rules run

Config redirects are checked before the filesystem, which includes pages and files in the public folder. They also run before the proxy file, so a matching rule answers the request and the proxy function never sees it.

That ordering is a feature. A fixed path mapping belongs in configuration where it is cheap and reviewable, and the proxy file stays reserved for decisions that need the request itself.

Two details are worth knowing before you write many rules. When a base path is configured, both the source and destination are prefixed with it automatically unless you opt a rule out. And a permanent flag applies to the response only, not to the config, so removing a rule does not clear the redirect from a browser that already cached it.

Hosting platforms sometimes cap how many rules a deployment can carry, often in the low thousands. When a list grows past that cap, the usual escape hatch is to move the lookup into the proxy file and read the mapping from a data store, so check your host's own limit before a large migration.

Verifying a redirect

The fastest check is a request that does not follow the redirect, so you can read the status code and destination directly.

bashbash
curl -I http://localhost:3000/team

The response should show the status you configured and a location header with the destination. If the status is 200 instead, the rule did not match, and the pattern is the first thing to inspect.

Next.js also ships an experimental helper that runs the config routing functions without a server, which makes redirect rules unit testable.

typescripttypescript
// next.config.test.ts
import { unstable_getResponseFromNextConfig, getRedirectUrl } from 'next/experimental/testing/server'
import nextConfig from './next.config'
 
test('retires the team page', async () => {
  const res = await unstable_getResponseFromNextConfig({ url: 'https://example.com/team', nextConfig })
  expect(getRedirectUrl(res)).toBe('https://example.com/about')
})

The helper evaluates only the config functions, so it proves the rule matches without proving how the deployed app behaves. Keep one real request check alongside it.

Common mistakes

Most broken redirect rules come from a small set of causes, and each has a recognizable symptom.

  • A source that also matches the destination, which produces a loop the browser reports after several hops.
  • A missing star modifier, so the rule covers one level and silently ignores nested paths.
  • A permanent flag used during an experiment, which browsers then cache past the end of the experiment.
  • A rule that expects to see a request Proxy already handled, when config redirects actually run first.

When a rule refuses to match, resist adding a second rule on top of it. Reproduce it with a single request first, since redirect loops are usually two rules disagreeing rather than one rule failing.

Rune AI

Rune AI

Key Insights

  • The redirects function returns objects with source, destination, and permanent fields.
  • Permanent true produces a 308 and false produces a 307, both of which preserve the request method.
  • Path parameters and modifiers let one rule cover a whole section of a site.
  • The has and missing arrays add header, cookie, query, and host conditions to a rule.
  • Config redirects run before the proxy file and before filesystem routes are checked.
RunePowered by Rune AI

Frequently Asked Questions

Why does Next.js return 308 instead of 301?

Because 308 preserves the request method and body, while many clients silently turn a 301 into a GET. Next.js uses 307 for temporary redirects for the same reason, so a POST to a redirected path stays a POST.

Are query strings carried to the destination?

Yes. Any query values on the incoming request are passed through to the destination automatically, so a request to an old path with tracking parameters keeps them after the redirect.

Do config redirects work in a static export?

No. Config redirects are evaluated by a server on each request, and a static export has no server to evaluate them. Handle those redirects at the CDN or hosting layer instead.

Do config redirects run before proxy?

Yes. The routing order applies config headers, then config redirects, then the proxy file. A matching config redirect answers the request before the proxy function ever runs.

Conclusion

Config redirects are the right tool for path mappings you know ahead of time. Declare them in the typed config file, use the permanent flag deliberately because a 308 is cached by clients, and verify each rule with a request that shows the status code and the location header.