redirects, rewrites & headers

1. redirects

Redirects allow you to redirect an incoming request path to a different destination path. When a user visits the source URL, they are automatically sent to the destination URL, and the browser's address bar updates to show the new location.

The redirects configuration in next.config.js is an async function that returns an array of redirect objects. Each redirect object specifies how incoming requests should be handled and where they should be sent.

Redirects are useful for:

Basic redirect configuration: module.exports = { async redirects() { return [ { source: '/about', destination: '/', permanent: true, }, ] }, }

Each redirect object requires three properties:

Next.js uses 307 and 308 status codes instead of traditional 302/301 to preserve the original HTTP method. This prevents browsers from changing POST requests to GET requests during redirection.

Path matching with parameters: module.exports = { async redirects() { return [ { source: '/old-blog/:slug', destination: '/news/:slug', permanent: true, }, // Wildcard matching { source: '/blog/:slug*', destination: '/news/:slug*', permanent: true, }, // Regex matching for numeric IDs { source: '/post/:id(\\d{1,})', destination: '/articles/:id', permanent: false, }, ] }, } Advanced redirects with conditional matching: module.exports = { async redirects() { return [ // Redirect based on header presence { source: '/:path*', has: [ { type: 'header', key: 'x-mobile-redirect', }, ], permanent: false, destination: '/mobile/:path*', }, // Redirect authenticated users { source: '/login', has: [ { type: 'cookie', key: 'authenticated', value: 'true', }, ], permanent: false, destination: '/dashboard', }, // Host-based redirects for multi-tenant apps { source: '/:path*', has: [ { type: 'host', value: 'admin.example.com', }, ], permanent: false, destination: '/admin/:path*', }, ] }, }

2. rewrites

Rewrites allow you to map an incoming request path to a different destination path while keeping the original URL in the browser's address bar. Unlike redirects, rewrites act as a URL proxy, masking the destination path so users don't see the URL change.

Rewrites are particularly powerful for:

Basic rewrite configuration: module.exports = { async rewrites() { return [ { source: '/about', destination: '/', }, ] }, }

Unlike redirects, rewrites only require source and destination properties. The user sees the source URL but gets content from the destination.

Advanced rewrite configuration with phases: module.exports = { async rewrites() { return { beforeFiles: [ // Override system files (runs before _next/public files) { source: '/sitemap.xml', destination: '/api/sitemap', }, ], afterFiles: [ // Handle missing pages (runs after pages/public check) { source: '/non-existent/:path*', destination: '/404-handler/:path*', }, ], fallback: [ // Final fallback for unmatched routes { source: '/:path*', destination: 'https://legacy-site.com/:path*', }, ], } }, } API proxying and external rewrites: module.exports = { async rewrites() { return [ // Proxy API requests to external service { source: '/api/users/:path*', destination: 'https://api.example.com/v1/users/:path*', }, // Clean URLs for blog posts { source: '/blog/:slug', destination: '/posts/:slug', }, // Multi-tenant routing { source: '/:tenant/dashboard/:path*', has: [ { type: 'host', value: 'app.example.com', }, ], destination: '/dashboard/:path*?tenant=:tenant', }, ] }, } Real-world example - Incremental migration: module.exports = { async rewrites() { return [ // New Next.js pages take precedence { source: '/products/:id', destination: '/api/products/:id', // New API endpoint }, // Fallback to legacy system for unmatched routes { source: '/:path*', destination: 'https://old-site.com/:path*', }, ] }, }

This pattern allows you to gradually migrate from a legacy system. New routes are handled by Next.js, while everything else falls back to the old site. Users experience no downtime during the migration process.

Parameter handling in rewrites: module.exports = { async rewrites() { return [ // Parameters not used in destination are passed as query params { source: '/search/:category/:query', destination: '/search', // :category and :query become ?category=...&query=... }, // Manual query parameter mapping { source: '/user/:id/posts/:postId', destination: '/posts/:postId?userId=:id', }, ] }, }

3. redirects

The headers configuration option in next.config.js allows you to define custom HTTP response headers based on request paths. This is particularly useful for setting security headers, cache controls, or custom metadata for specific routes.

Headers are evaluated before the filesystem, including static pages and public files.

module.exports = { async headers() { return [ { source: '/about', headers: [ { key: 'x-custom-header', value: 'my custom header value' }, { key: 'x-another-custom-header', value: 'my other custom header value' }, ], }, ]; }, };

Each object in the returned array requires:


3.1. Header Overriding Behavior

When multiple headers apply to the same path and set the same header key, the last one overrides the previous:

module.exports = { async headers() { return [ { source: '/:path*', headers: [{ key: 'x-hello', value: 'there' }], }, { source: '/hello', headers: [{ key: 'x-hello', value: 'world' }], }, ]; }, };

Visiting /hello returns x-hello: world.


3.2. Path Matching

Headers support dynamic and wildcard path matching:

module.exports = { async headers() { return [ { source: '/blog/:slug', headers: [ { key: 'x-slug', value: ':slug' }, { key: 'x-slug-:slug', value: 'example' }, ], }, ]; }, };

/blog/hello-world returns x-slug: hello-world and x-slug-hello-world: example.


3.3. Wildcard Matching

Use :slug* to match nested paths:

{ source: '/blog/:slug*', headers: [ { key: 'x-slug', value: ':slug*' }, { key: 'x-slug-:slug*', value: 'wild' }, ], }
3.4. Regex Path Matching

Regex patterns allow fine-grained matching:

{ source: '/blog/:post(\\d{1,})', headers: [ { key: 'x-post', value: ':post' }, ], }

This matches /blog/123 but not /blog/abc.


3.5. Escaping Special Characters

Escape special regex characters using \\:

{ source: '/english\\(default\\)/:slug', headers: [{ key: 'x-header', value: 'value' }], }
3.6. Matching Header, Cookie, Query, and Host

Use has and missing arrays to conditionally apply headers:

{ source: '/:path*', has: [ { type: 'header', key: 'x-add-header' } ], headers: [ { key: 'x-another-header', value: 'hello' } ], } { source: '/specific/:path*', has: [ { type: 'query', key: 'page', value: 'home' }, { type: 'cookie', key: 'authorized', value: 'true' }, ], headers: [ { key: 'x-authorized', value: ':authorized' }, ], }

Regex groups can also be used:

{ source: '/:path*', has: [ { type: 'header', key: 'x-authorized', value: '(?yes|true)' } ], headers: [ { key: 'x-another-header', value: ':authorized' } ], }

Host matching:

{ source: '/:path*', has: [ { type: 'host', value: 'example.com' } ], headers: [ { key: 'x-another-header', value: ':authorized' } ], }
3.7. basePath and locale

When basePath is set, all paths are prefixed unless basePath: false is used.

module.exports = { basePath: '/docs', async headers() { return [ { source: '/with-basePath', // becomes /docs/with-basePath headers: [{ key: 'x-hello', value: 'world' }], }, { source: '/without-basePath', basePath: false, headers: [{ key: 'x-hello', value: 'world' }], }, ]; }, };

When i18n is configured, locale prefixes are automatically handled unless locale: false is set.

module.exports = { i18n: { locales: ['en', 'fr', 'de'], defaultLocale: 'en', }, async headers() { return [ { source: '/with-locale', headers: [{ key: 'x-hello', value: 'world' }], }, { source: '/nl/with-locale-manual', locale: false, headers: [{ key: 'x-hello', value: 'world' }], }, ]; }, };
3.8. Security Headers (Real-World Use)

Common security headers:

{ source: '/:path*', headers: [ { key: 'Strict-Transport-Security', value: 'max-age=63072000; includeSubDomains; preload' }, { key: 'X-Frame-Options', value: 'SAMEORIGIN' }, { key: 'X-Content-Type-Options', value: 'nosniff' }, { key: 'Permissions-Policy', value: 'camera=(), microphone=(), geolocation=(), browsing-topics=()' }, { key: 'Referrer-Policy', value: 'origin-when-cross-origin' }, { key: 'X-DNS-Prefetch-Control', value: 'on' }, ], }
3.9. CORS Headers for APIs
{ source: '/api/:path*', headers: [ { key: 'Access-Control-Allow-Origin', value: '*' }, { key: 'Access-Control-Allow-Methods', value: 'GET, POST, PUT, DELETE, OPTIONS' }, { key: 'Access-Control-Allow-Headers', value: 'Content-Type, Authorization' }, ], }
3.10. Cache-Control

Cache headers can be set manually for non-static assets (note: immutable assets can't be overridden):

{ source: '/custom-data', headers: [ { key: 'Cache-Control', value: 'public, max-age=86400' }, ], }

For immutable static assets like images with hashes, Next.js automatically applies: Cache-Control: public, max-age=31536000, immutable.