MENU
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:
- Moving content to new URLs while preserving SEO value
- Handling legacy URLs from site migrations
- Enforcing canonical URLs (e.g., removing trailing slashes)
- Redirecting users based on authentication status or location
module.exports = {
async redirects() {
return [
{
source: '/about',
destination: '/',
permanent: true,
},
]
},
}
Each redirect object requires three properties:
- source - the incoming request path pattern to match
- destination - the path where users should be redirected
- permanent - boolean determining HTTP status code (308 for permanent, 307 for temporary)
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:
- Creating clean, user-friendly URLs that map to complex backend paths
- Proxying requests to external APIs or microservices
- Gradually migrating from legacy systems
- A/B testing by routing traffic to different implementations
- Implementing custom routing logic without exposing internal structure
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:
- source: A path pattern the header should apply to.
- headers: An array of key-value pairs for the HTTP headers to be set.
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.