Skip to main content
Next.js provides several ways to handle redirects depending on your use case:
APIPurposeWhereStatus code
redirect()Redirect after a mutation or eventServer Components, Server Actions, Route Handlers307 or 303
permanentRedirect()Permanent redirect after a canonical URL changeServer Components, Server Actions, Route Handlers308
useRouter()Client-side navigationEvent handlers in Client ComponentsN/A
redirects in next.config.jsRedirect based on path patternsConfig file307 or 308
NextResponse.redirectRedirect based on conditionsMiddlewareAny

redirect()

Use redirect() in Server Components, Route Handlers, and Server Actions. It returns a 307 (Temporary Redirect) by default, or 303 (See Other) from a Server Action.
'use server'

import { redirect } from 'next/navigation'
import { revalidatePath } from 'next/cache'

export async function createPost(id: string) {
  try {
    // Call database
  } catch (error) {
    // Handle errors
  }

  revalidatePath('/posts')
  redirect(`/post/${id}`) // Navigate to the new post
}
redirect throws internally, so call it outside try/catch blocks. It also accepts absolute URLs for external redirects.

permanentRedirect()

Use permanentRedirect() when a resource’s canonical URL changes permanently (for example, after a username change). It returns a 308 status code.
'use server'

import { permanentRedirect } from 'next/navigation'
import { revalidateTag } from 'next/cache'

export async function updateUsername(username: string, formData: FormData) {
  try {
    // Call database
  } catch (error) {
    // Handle errors
  }

  revalidateTag('username')
  permanentRedirect(`/profile/${username}`)
}

useRouter() hook

For programmatic navigation in Client Component event handlers:
'use client'

import { useRouter } from 'next/navigation'

export default function Page() {
  const router = useRouter()

  return (
    <button type="button" onClick={() => router.push('/dashboard')}>
      Dashboard
    </button>
  )
}
Prefer the <Link> component for navigation that doesn’t require programmatic control.

redirects in next.config.js

Define static redirect rules in your config file. These run before middleware and support path, header, cookie, and query matching:
import type { NextConfig } from 'next'

const nextConfig: NextConfig = {
  async redirects() {
    return [
      // Basic redirect
      {
        source: '/about',
        destination: '/',
        permanent: true,
      },
      // Wildcard path matching
      {
        source: '/blog/:slug',
        destination: '/news/:slug',
        permanent: true,
      },
    ]
  },
}

export default nextConfig
  • permanent: true → 308 status code
  • permanent: false → 307 status code
Platforms may limit the number of redirects entries. On Vercel, the limit is 1,024. For larger redirect sets, use middleware with a redirect map.

NextResponse.redirect in middleware

Use middleware to redirect based on conditions like authentication or feature flags:
import { NextResponse, NextRequest } from 'next/server'
import { authenticate } from 'auth-provider'

export function middleware(request: NextRequest) {
  const isAuthenticated = authenticate(request)

  if (isAuthenticated) return NextResponse.next()

  return NextResponse.redirect(new URL('/login', request.url))
}

export const config = {
  matcher: '/dashboard/:path*',
}
Middleware runs after redirects in next.config.js and before rendering.

Managing redirects at scale

For 1,000+ redirects, avoid storing them all in next.config.js. Instead, use middleware with a database or a Bloom filter for efficient lookups:

Redirect map with middleware

Store redirects in a key-value store and look them up in middleware:
import { NextResponse, NextRequest } from 'next/server'
import { get } from '@vercel/edge-config'

type RedirectEntry = {
  destination: string
  permanent: boolean
}

export async function middleware(request: NextRequest) {
  const pathname = request.nextUrl.pathname
  const redirectData = await get(pathname)

  if (redirectData && typeof redirectData === 'string') {
    const redirectEntry: RedirectEntry = JSON.parse(redirectData)
    const statusCode = redirectEntry.permanent ? 308 : 307
    return NextResponse.redirect(redirectEntry.destination, statusCode)
  }

  return NextResponse.next()
}

Bloom filter optimization

For very large redirect sets, use a Bloom filter to check if a redirect might exist before querying the database:
import { NextResponse, NextRequest } from 'next/server'
import { ScalableBloomFilter } from 'bloom-filters'
import GeneratedBloomFilter from './redirects/bloom-filter.json'

const bloomFilter = ScalableBloomFilter.fromJSON(GeneratedBloomFilter as any)

export async function middleware(request: NextRequest) {
  const pathname = request.nextUrl.pathname

  if (bloomFilter.has(pathname)) {
    const api = new URL(
      `/api/redirects?pathname=${encodeURIComponent(pathname)}`,
      request.nextUrl.origin
    )

    try {
      const redirectData = await fetch(api)
      if (redirectData.ok) {
        const { destination, permanent } = await redirectData.json()
        if (destination) {
          return NextResponse.redirect(destination, permanent ? 308 : 307)
        }
      }
    } catch (error) {
      console.error(error)
    }
  }

  return NextResponse.next()
}
The Route Handler reads from a static JSON file and accounts for Bloom filter false positives:
import { NextRequest, NextResponse } from 'next/server'
import redirects from '@/app/redirects/redirects.json'

type RedirectEntry = { destination: string; permanent: boolean }

export function GET(request: NextRequest) {
  const pathname = request.nextUrl.searchParams.get('pathname')
  if (!pathname) return new Response('Bad Request', { status: 400 })

  const redirect = (redirects as Record<string, RedirectEntry>)[pathname]
  if (!redirect) return new Response('No redirect', { status: 400 })

  return NextResponse.json(redirect)
}

Build docs developers (and LLMs) love