Skip to content

Redirects in Next.js

Next.js has four places where you can redirect: the config file, middleware, server code in the App Router and the trailingSlash option. Each one sends different status codes by default, so it pays to know which is which.

Overview

MethodDefault statusUse it for
redirects() in next.config.js308 (permanent: true) or 307Static, known URL mappings
Middleware / Proxy with NextResponse.redirect()307Conditions at request time: cookies, geo, large redirect maps
redirect() in the App Router307 (303 in Server Actions)Redirects after logic in Server Components, Route Handlers, Server Actions
permanentRedirect()308A resource has moved for good, decided in server code
trailingSlash308Normalizing URLs with or without a trailing slash

Next.js prefers 307 and 308 over 302 and 301 because they guarantee that the request method is kept: a POST stays a POST. For search engines, 308 is equivalent to 301 and 307 to 302. Background is in 307 vs. 308.

redirects() in next.config.js

The redirects function in your config returns an array of rules. They are evaluated before the file system, so they also work for URLs that no longer have a page:

/** @type {import('next').NextConfig} */
const nextConfig = {
  async redirects() {
    return [
      // 308 Permanent Redirect
      {
        source: '/about-us',
        destination: '/about',
        permanent: true,
      },
      // Path parameter
      {
        source: '/blog/:slug',
        destination: '/news/:slug',
        permanent: true,
      },
      // Wildcard: matches /docs/a/b/c
      {
        source: '/docs/:path*',
        destination: 'https://docs.example.com/:path*',
        permanent: false,
      },
      // Explicit status code instead of permanent
      {
        source: '/old-shop',
        destination: '/shop',
        statusCode: 301,
      },
    ]
  },
}

module.exports = nextConfig

A few rules to remember:

  • permanent: true sends 308, permanent: false sends 307. If you need a classic 301 for an old client, use statusCode instead of permanent, not both.
  • Query strings are passed on to the destination automatically.
  • With has and missing you can match on headers, cookies, query parameters or the host. That is how you redirect www to non-www inside Next.js:
{
  source: '/:path*',
  has: [{ type: 'host', value: 'www.example.com' }],
  destination: 'https://example.com/:path*',
  permanent: true,
}

If you host on Vercel, a domain redirect in the dashboard is simpler for this case, see Vercel and Netlify redirects. With next.config.ts the syntax is the same; you just export default nextConfig. Note that redirects() does not work with output: 'export', because a static export has no server that could send a status code.

Middleware: NextResponse.redirect()

Middleware runs before a route is rendered and can decide per request. Since Next.js 16 the file is called proxy.ts and the exported function proxy; in earlier versions it is middleware.ts with a function called middleware. The API is the same:

// proxy.ts (Next.js 16) or middleware.ts (Next.js 15 and older)
import { NextResponse } from 'next/server'
import type { NextRequest } from 'next/server'

export function proxy(request: NextRequest) {
  if (!request.cookies.has('session')) {
    // 307 by default
    return NextResponse.redirect(new URL('/login', request.url))
  }

  if (request.nextUrl.pathname === '/old-pricing') {
    // explicit permanent redirect
    return NextResponse.redirect(new URL('/pricing', request.url), 308)
  }

  return NextResponse.next()
}

export const config = {
  matcher: ['/dashboard/:path*', '/old-pricing'],
}

NextResponse.redirect() needs an absolute URL, which is why the example builds it with new URL(path, request.url). Restrict the middleware with a matcher so it doesn't run on every image and static file.

Middleware is also the recommended place for large redirect lists (thousands of entries): load a map from a JSON file or an edge store, look up the pathname and redirect if there is a match. The config file is not meant for huge lists.

redirect() and permanentRedirect() in the App Router

In Server Components, Route Handlers and Server Actions you import the functions from next/navigation:

// app/products/[id]/page.tsx
import { redirect, permanentRedirect, notFound } from 'next/navigation'

export default async function Page({ params }: { params: Promise<{ id: string }> }) {
  const { id } = await params
  const product = await getProduct(id)

  if (!product) {
    notFound()
  }

  if (product.slug !== id) {
    // moved for good: 308
    permanentRedirect(`/products/${product.slug}`)
  }

  if (!product.published) {
    // temporary: 307
    redirect('/products')
  }

  return <h1>{product.name}</h1>
}
  • redirect() sends 307; in a Server Action it sends 303 See Other, so the browser follows up with a GET.
  • permanentRedirect() sends 308.
  • Both throw internally, so you don't need return. Don't call them inside a try block, or your catch will swallow the redirect.
  • If the response is already streaming, the status code can no longer change. Next.js then inserts a meta tag that redirects on the client. Crawlers treat that differently from a real HTTP redirect, so trigger redirects before any Suspense boundary starts streaming. Check the result with the redirect checker: it shows whether you get a 308 or a meta refresh.

In the Pages Router, the equivalent is returning { redirect: { destination: '/login', permanent: false } } from getServerSideProps or getStaticProps.

trailingSlash

By default Next.js redirects /about/ to /about with a 308. With trailingSlash: true it does the opposite:

module.exports = {
  trailingSlash: true, // /about -> /about/ (308)
}

Choose one form and keep internal links consistent with it; otherwise every internal link costs an extra hop. If a redirect in redirects() points to the "wrong" form, the trailing slash redirect is added on top and you get a redirect chain. With skipTrailingSlashRedirect: true you can switch the automatic redirect off and handle it yourself in middleware.

Redirects vs. rewrites

Both use the same source/destination syntax, but they do different things:

RedirectRewrite
URL in the browserChanges to the destinationStays the same
HTTP response3xx with Location header200 with the content of the destination
Search enginesIndex the destinationIndex the original URL
Typical useMoved content, domain changesProxying an API, clean URLs, multi-zone apps
async rewrites() {
  return [
    { source: '/api/:path*', destination: 'https://api.example.com/:path*' },
  ]
}

If a URL has really moved, use a redirect. A rewrite would leave the old URL indexed and serve duplicate content under two addresses.

Order of evaluation

Next.js processes a request in this order: headers, redirects, middleware/proxy, beforeFiles rewrites, file system routes, afterFiles rewrites, dynamic routes, fallback rewrites. A rule in redirects() therefore wins over a page with the same path and over your middleware.

Testing

next dev doesn't always behave like production, especially behind a CDN. After deploying, run the most important URLs through the redirect checker and look at the status code of every hop. For many URLs at once, use the bulk checker. To learn more about the status codes Next.js sends, see 301 vs. 302.

Frequently asked questions

Why does Next.js send 308 instead of 301?

308 is the permanent redirect that keeps the request method, while browsers may turn a POST into a GET after a 301. Google treats 308 like 301. If you need a 301 anyway, set statusCode: 301 instead of permanent.

What is the difference between redirect() and permanentRedirect()?

redirect() sends a temporary 307 (or 303 in Server Actions), permanentRedirect() a permanent 308. Use the permanent one only if the old URL should disappear from search results.

Why is my redirect() not working inside try/catch?

redirect() works by throwing a special error that Next.js catches. A surrounding catch intercepts it. Call redirect() after the try/catch block, or rethrow the error.

Do redirects in next.config.js work with a static export?

No. With output: 'export' there is no Next.js server to send status codes. Configure the redirects on your host instead, for example in vercel.json, Netlify's _redirects, .htaccess or nginx.

  • .htaccess redirects on Apache

    Everything you need to redirect pages and whole domains with .htaccess on Apache: mod_alias vs. mod_rewrite, copy-ready rules for the common cases, and how to avoid loops and chains.

  • nginx redirects

    Copy-ready nginx configuration for 301 redirects: single pages, patterns, whole domains, HTTPS and www in a single hop, and hundreds of URLs with map.

  • Redirects on Cloudflare

    Cloudflare can answer redirects at the edge before a request ever reaches your server. Here is how Single Redirects, Bulk Redirects and Always Use HTTPS work, and how to keep them from fighting with your origin.

  • Redirects in WordPress

    WordPress gives you several ways to redirect a URL: a plugin, a rule in .htaccess or a few lines of PHP. This guide shows when to use which, and how to avoid the typical pitfalls with caching and HTTPS.

  • Redirects on Vercel and Netlify

    On Vercel and Netlify you don't touch a web server config. Redirects live in a file in your repository or in the dashboard, and the platform's edge network sends them. Here is how both work and where they differ.

  • Redirects on Microsoft IIS

    IIS gives you two ways to redirect: the built-in HTTP Redirect feature and the URL Rewrite module. Here is when to use which, with web.config examples you can copy.