Configuring Next.js (next.config.js)
next.config.js is a Node.js module at your project root that Next.js reads once at startup to configure bundling, routing, image optimization, and deployment behavior.
Theory
TL;DR
- Think of it like a car dashboard: set it once, every build uses those settings
- Next.js reads
next.config.jsviarequire()at startup, not on every request - Changes require a dev server restart (
next dev/next build) - Client-side env vars need the
NEXT_PUBLIC_prefix or an explicitenvkey; never put secrets there - Default format is CJS (
module.exports); Next.js 14.2+ supportsnext.config.tswith full type checking
Quick example
// next.config.js - basic production setup
/** @type {import('next').NextConfig} */
const nextConfig = {
images: {
remotePatterns: [{ protocol: 'https', hostname: '**.example.com' }]
},
basePath: '/my-app', // /about becomes /my-app/about
reactStrictMode: true,
};
module.exports = nextConfig;After next dev: images from *.example.com load through Next.js optimization, and all routes gain the /my-app prefix automatically.
How Next.js loads the config
Next.js CLI calls Node.js require() on next.config.js once per dev session or build. It validates the exported object, then passes values into SWC/Webpack compilers and the server runtime. Values from the env key go through Webpack's DefinePlugin, which inlines them as string literals in the bundle. That is why they are fixed at build time, not evaluated per request.
Hot reload watches the file and restarts the dev server on changes. During next build, the config is serialized for static export if output: 'export' is set.
Core options
Images. The images key controls Next.js image optimization. Use remotePatterns (not the deprecated domains) to allow external sources:
images: {
remotePatterns: [
{ protocol: 'https', hostname: 'cdn.shopify.com', pathname: '/**' },
{ protocol: 'https', hostname: 'images.unsplash.com' },
],
minimumCacheTTL: 60,
formats: ['image/avif', 'image/webp'],
},remotePatterns adds protocol and pathname filters that domains never had, which is why domains was deprecated in Next.js 13.
Redirects and rewrites. A redirect sends the browser to a new URL with a status code. A rewrite proxies the request without changing the URL the user sees:
async redirects() {
return [
{ source: '/old-blog/:slug', destination: '/blog/:slug', permanent: true },
];
},
async rewrites() {
return [
{ source: '/api/:path*', destination: 'https://api.example.com/:path*' },
];
},The rewrite-based API proxy is the pattern most production Next.js apps end up using. It keeps API keys server-side and avoids CORS without touching the backend configuration at all.
Headers. Add security headers globally or per route:
async headers() {
return [
{
source: '/(.*)',
headers: [
{ key: 'X-Frame-Options', value: 'DENY' },
{ key: 'X-Content-Type-Options', value: 'nosniff' },
],
},
];
},Output and deployment. output: 'standalone' bundles only the files your app needs, which is what most Docker setups use. output: 'export' generates a fully static site for S3, GitHub Pages, or Vercel's static tier.
Environment variables. Two ways to expose vars at build time:
env: { API_URL: process.env.API_URL }This inlines the value into the client bundle. For server-only vars, skip this key entirely and read process.env directly inside server components or API routes.
When to use each option
- External images from a CDN or CMS:
images.remotePatterns - API proxy to avoid CORS:
rewrites - Old URLs that moved permanently:
redirectswithpermanent: true - Security headers (CSP, HSTS):
headers - Docker deployment:
output: 'standalone' - Subdirectory hosting (e.g., behind Nginx at
/app):basePath - Third-party packages that break RSC:
experimental.serverComponentsExternalPackages - MDX or custom file extensions:
pageExtensions: ['js', 'jsx', 'md', 'mdx']
Common mistakes
Editing config without restarting the dev server. next.config.js loads once. If you add remotePatterns and images still fail, that is why. Restart next dev. With Turbopack in Next.js 15+ (next dev --turbo), restarts are faster but still required for config changes.
Exposing secrets through env.
// Wrong - this bundles DB_PASS into the client JavaScript
env: { DB_PASS: process.env.DB_PASS }Access server-only values via process.env.DB_PASS inside a server component. The NEXT_PUBLIC_ prefix exists for values that are genuinely safe to expose.
Wildcard hostname in remotePatterns.
// Wrong - Next.js 13+ rejects '*' as a hostname
remotePatterns: [{ hostname: '*' }]List exact hostnames. For subdomains, use ** as a prefix: hostname: '**.example.com'.
Async config in CommonJS. module.exports = async () => ({}) crashes in older Next.js versions. Use a sync export in .js files, or move to next.config.mjs (ESM) if you need async logic.
Setting typescript.ignoreBuildErrors: true in production. This skips type checking during next build and ships broken code to users. Keep it false and fix actual errors.
Real-world usage
- Vercel Commerce / Shopify Hydrogen:
images.remotePatternsfor CDN assets +rewritesfor the Storefront API - T3 Stack (tRPC + Next.js):
experimental.serverComponentsExternalPackages: ['@trpc/server'] - Content sites with MDX:
pageExtensions: ['js', 'jsx', 'md', 'mdx'] - Docker deployments:
output: 'standalone' - Supabase apps:
rewritesto proxy edge functions without CORS
Follow-up questions
Q: What is the difference between images.domains and images.remotePatterns?
A: domains (deprecated in Next.js 13) allows any path on a hostname with no filters. remotePatterns lets you restrict by protocol, pathname, and port. Always use remotePatterns.
Q: How does next.config.js interact with Turbopack?
A: Turbopack (stable in Next.js 15) ignores most webpack config keys. To add custom loaders, use turbo.rules instead: turbo: { rules: { '*.sql': { loaders: ['sql-loader'] } } }.
Q: Can next.config.js export an async function?
A: In CommonJS (.js), only sync exports work reliably. In ESM (next.config.mjs) or TypeScript (next.config.ts, Next.js 14.2+), async exports are supported, but avoid I/O operations at build time.
Q: Why does changing basePath break static asset imports?
A: Static files in /public need the basePath prefix too. next/image handles this automatically, but a raw <img src="/logo.png"> does not. Fix it with assetPrefix or switch to the next/image component.
Q: In a monorepo with multiple Next.js apps, how do you share config without duplication?
A: Extract shared options into a package (e.g., packages/next-config/base.mjs) and spread them: import base from '@acme/next-config'; export default { ...base, ...localOverrides };. This is the pattern used in Turborepo's Next.js starters.
Examples
Basic: image CDN and API proxy
// next.config.js - common production setup
/** @type {import('next').NextConfig} */
const nextConfig = {
reactStrictMode: true,
images: {
remotePatterns: [
{ protocol: 'https', hostname: 'images.unsplash.com' },
{ protocol: 'https', hostname: 'cdn.shopify.com', pathname: '/**' },
],
minimumCacheTTL: 60,
},
async rewrites() {
return [
// Browser calls /api/products, Next.js forwards to the backend
{ source: '/api/:path*', destination: 'https://api.example.com/:path*' },
];
},
};
module.exports = nextConfig;/api/products on the client resolves without CORS errors. Shopify images are auto-resized and served in WebP or AVIF format.
Intermediate: Docker output, security headers, and TypeScript config
// next.config.ts (Next.js 14.2+)
import type { NextConfig } from 'next';
const isProd = process.env.NODE_ENV === 'production';
const nextConfig: NextConfig = {
output: 'standalone', // Minimal Docker image
reactStrictMode: true,
async headers() {
return [
{
source: '/(.*)',
headers: [
{ key: 'X-Frame-Options', value: 'DENY' },
{ key: 'X-Content-Type-Options', value: 'nosniff' },
// Skip HSTS in dev - localhost uses HTTP
...(isProd
? [{ key: 'Strict-Transport-Security', value: 'max-age=63072000' }]
: []),
],
},
];
},
typescript: {
ignoreBuildErrors: false, // Never set to true in production
},
};
export default nextConfig;In dev, HSTS is skipped so localhost still works over HTTP. In production, the header enforces HTTPS for 2 years. The standalone output keeps the Docker image small by including only what the app actually uses.
Advanced: shared config across a monorepo
// packages/next-config/base.mjs - shared across all apps
export const baseConfig = {
reactStrictMode: true,
images: {
formats: ['image/avif', 'image/webp'],
},
experimental: {
optimizePackageImports: ['lucide-react'],
},
};
// apps/storefront/next.config.mjs
import { baseConfig } from '@acme/next-config';
/** @type {import('next').NextConfig} */
export default {
...baseConfig,
images: {
...baseConfig.images,
remotePatterns: [
{ protocol: 'https', hostname: 'cdn.shopify.com', pathname: '/**' },
],
},
async rewrites() {
return [
{ source: '/api/:path*', destination: process.env.API_URL + '/:path*' },
];
},
};Each app in the monorepo extends the shared config without repeating reactStrictMode, formats, or experimental. The API_URL env var points to a different backend per environment.
Short Answer
Interview readyA concise answer to help you respond confidently on this topic during an interview.