Type something to search...
A Deep Dive into next.config Options Every Developer Should Know

A Deep Dive into next.config Options Every Developer Should Know

next.config.ts is the one file every Next.js project has and almost nobody reads end to end. It starts as an empty object, then slowly collects a redirect here, an image domain there, and a flag someone copied from a GitHub issue two years ago. The official reference lists dozens of options, and most of them you'll never touch. A handful, though, change how your app routes, renders, caches, and deploys, and it's worth knowing those well.

This post walks through the options that come up in real projects, grouped by what they do: routing (redirects, rewrites, headers), images, output and deployment, caching and rendering, how packages get bundled, security, and developer experience. All examples target Next.js 16, where a few options were removed or had their defaults changed.

The Config File Itself

Next.js looks for next.config.js, next.config.mjs, or next.config.ts in the project root. With TypeScript, use the NextConfig type so your editor autocompletes options and flags typos:

// next.config.ts
import type { NextConfig } from "next";

const nextConfig: NextConfig = {
  // options go here
};

export default nextConfig;

The file runs in Node.js during next dev, next build, and next start. It's never sent to the browser, so it's safe to read process.env here. Note that .cjs and .cts extensions aren't supported.

Config as a Function

You can export a function instead of an object. It receives the current phase so you can vary the config between development and production:

// next.config.ts
import type { NextConfig } from "next";
import { PHASE_DEVELOPMENT_SERVER } from "next/constants";

export default function config(phase: string): NextConfig {
  const isDev = phase === PHASE_DEVELOPMENT_SERVER;

  return {
    logging: isDev ? { fetches: { fullUrl: true } } : undefined,
    productionBrowserSourceMaps: !isDev,
  };
}

The function can also be async, which is handy if you need to load redirects from a file or an API at build time. Keep it fast, though: it runs every time the dev server or build starts.

Routing: Redirects, Rewrites, and Headers

These three options let you shape URLs and responses without writing any route code. They're evaluated before your pages and before files in public/.

redirects

Use redirects when a URL has moved and you want the browser (and search engines) to follow:

// next.config.ts
import type { NextConfig } from "next";

const nextConfig: NextConfig = {
  async redirects() {
    return [
      {
        source: "/old-blog/:slug",
        destination: "/blog/:slug",
        permanent: true,
      },
      {
        source: "/docs",
        destination: "/docs/getting-started",
        permanent: false,
      },
    ];
  },
};

export default nextConfig;

permanent: true sends a 308, which browsers and crawlers cache. permanent: false sends a 307. Query strings are passed through automatically, so /old-blog/hello?ref=x lands on /blog/hello?ref=x.

Path matching supports named parameters (:slug), wildcards (:path* for zero or more segments), and regex (:id(\\d+)). You can also match conditionally with has and missing:

{
  source: "/dashboard/:path*",
  missing: [{ type: "cookie", key: "session" }],
  destination: "/login",
  permanent: false,
}

That's handy for simple cases, but anything involving real auth logic belongs in proxy.ts. If you're moving a lot of URLs at once, a redirect list in config is easier to review than code. For SEO implications of 308 vs 307, see the benefits of Next.js for SEO.

rewrites

A rewrite serves content from a different path while keeping the URL in the address bar unchanged. The classic use is proxying an external API to avoid CORS:

// next.config.ts
import type { NextConfig } from "next";

const nextConfig: NextConfig = {
  async rewrites() {
    return [
      {
        source: "/api/legacy/:path*",
        destination: "https://legacy.example.com/:path*",
      },
    ];
  },
};

export default nextConfig;

When rewrites returns a plain array, the rules run after checking pages and public/ files. If you need finer control, return an object with beforeFiles, afterFiles, and fallback arrays. fallback is useful for incremental migrations: anything Next.js can't serve gets proxied to the old app.

async rewrites() {
  return {
    beforeFiles: [],
    afterFiles: [],
    fallback: [
      { source: "/:path*", destination: "https://old-site.example.com/:path*" },
    ],
  };
}

headers

Set response headers by path. Security headers are the most common use:

// next.config.ts
import type { NextConfig } from "next";

const securityHeaders = [
  { key: "X-Content-Type-Options", value: "nosniff" },
  { key: "Referrer-Policy", value: "strict-origin-when-cross-origin" },
  { key: "X-Frame-Options", value: "DENY" },
  {
    key: "Strict-Transport-Security",
    value: "max-age=63072000; includeSubDomains; preload",
  },
];

const nextConfig: NextConfig = {
  async headers() {
    return [
      { source: "/:path*", headers: securityHeaders },
      {
        source: "/fonts/:file*",
        headers: [
          {
            key: "Cache-Control",
            value: "public, max-age=31536000, immutable",
          },
        ],
      },
    ];
  },
};

export default nextConfig;

If two rules set the same header for a path, the later one wins. Content Security Policy with per-request nonces can't be done here because the value must change on every request; that belongs in proxy.ts.

Images

The images key configures the next/image optimizer. Next.js 16 tightened several defaults for security, so older configs may need updates.

// next.config.ts
import type { NextConfig } from "next";

const nextConfig: NextConfig = {
  images: {
    remotePatterns: [
      new URL("https://cdn.example.com/uploads/**"),
      {
        protocol: "https",
        hostname: "**.githubusercontent.com",
        pathname: "/**",
      },
    ],
    formats: ["image/avif", "image/webp"],
    qualities: [60, 75, 90],
    minimumCacheTTL: 14400,
  },
};

export default nextConfig;

What each option does:

  • remotePatterns: an allowlist of external image URLs. Without a match, remote images fail. You can pass a URL object or an object with protocol, hostname, port, pathname, and search. ** matches any number of subdomains at the start or path segments at the end. The older domains option is deprecated; switch to remotePatterns.
  • localPatterns: the same idea for local paths. In v16, local images with query strings (like /photo.png?v=2) require a matching localPatterns entry with a search value.
  • formats: output formats in order of preference. AVIF is smaller but slower to encode; listing both means each format is cached separately.
  • qualities: in v16 this defaults to [75] only. A quality prop that isn't in the list is coerced to the closest allowed value, so add every quality you actually use.
  • minimumCacheTTL: the minimum time in seconds an optimized image is cached. The default changed from 60 seconds to 4 hours in v16.

Two other v16 changes: optimizing images from local IP addresses is blocked unless you set dangerouslyAllowLocalIP: true (only for private networks), and maximumRedirects limits how many redirects the optimizer follows when fetching a remote image. If you use a third-party image CDN instead, set loader: "custom" and loaderFile to a function that builds the URL. The post on optimizing images with next/image covers the component side.

Output and Deployment

output

This decides what next build produces:

ValueResultUse it for
(unset).next folder, run with next startVercel, most Node hosts
"standalone".next/standalone with a minimal server.js and only the needed node_modulesDocker, self-hosting
"export"Plain HTML/CSS/JS in out/Static hosts, no Node server

standalone uses output file tracing to copy only the files your server needs, which makes Docker images dramatically smaller. It doesn't copy public/ or .next/static; you copy those yourself or serve them from a CDN. export drops all server features: no Route Handlers that read requests, no Server Actions, no proxy.ts, no default image optimization.

Output File Tracing in Monorepos

In a monorepo, tracing starts at the app folder by default, so shared packages outside it can be missed. Point the tracing root at the repo root:

// apps/web/next.config.ts
import path from "node:path";
import type { NextConfig } from "next";

const nextConfig: NextConfig = {
  output: "standalone",
  outputFileTracingRoot: path.join(import.meta.dirname, "../../"),
  outputFileTracingIncludes: {
    "/api/reports": ["./templates/**/*"],
  },
};

export default nextConfig;

outputFileTracingIncludes and outputFileTracingExcludes take route globs as keys and file globs as values. Use them when a route reads files dynamically (templates, fonts for PDF generation, native binaries) that static analysis can't detect. import.meta.dirname requires Node 20.11 or later; you can use path.resolve() with a known relative path otherwise.

basePath, assetPrefix, and trailingSlash

  • basePath: "/docs" serves the whole app under a sub-path. next/link and redirect() add the prefix automatically, but plain img tags and next/image with string paths need the prefix written in.
  • assetPrefix: "https://cdn.example.com" loads /_next/static assets from a CDN while pages are served from your origin.
  • trailingSlash: true redirects /about to /about/. With output: "export" it also changes the file layout to about/index.html, which some static hosts need.

deploymentId and generateBuildId

When you run several instances behind a load balancer, or roll out a new version while users still have old tabs open, these help Next.js detect version skew:

const nextConfig: NextConfig = {
  deploymentId: process.env.DEPLOYMENT_VERSION,
  generateBuildId: async () => process.env.GIT_SHA ?? null,
};

A consistent build ID across containers built from the same commit avoids mismatched asset requests. Returning null from generateBuildId falls back to the default random ID.

Caching and Rendering

cacheComponents

This is the big switch in Next.js 16:

const nextConfig: NextConfig = {
  cacheComponents: true,
};

With it on, data fetching is dynamic by default, you opt into caching with the "use cache" directive, and Partial Prerendering becomes the default rendering model: a static shell is prerendered and dynamic parts stream in. It replaces the older experimental.ppr, experimental.dynamicIO, and experimental.useCache flags. It also changes client navigation to keep previous routes mounted (hidden) so state survives back and forward navigation. Read the use cache guide before flipping it on an existing app.

cacheLife

Custom cache profiles for cacheLife() calls:

const nextConfig: NextConfig = {
  cacheComponents: true,
  cacheLife: {
    blog: {
      stale: 3600,
      revalidate: 900,
      expire: 86400,
    },
  },
};

stale is how long the client may use the value without checking, revalidate is how often the server refreshes it in the background, and expire is the hard limit after which a fresh fetch is required. In code you then call cacheLife("blog") inside a "use cache" scope.

reactCompiler

The React Compiler automatically memoizes components and hooks, so you write fewer useMemo and useCallback calls. It's a top-level option now:

const nextConfig: NextConfig = {
  reactCompiler: true,
};

You also need babel-plugin-react-compiler installed as a dev dependency. Next.js only runs the compiler on files that contain JSX or hooks, so the build-time cost is modest. You can pass { compilationMode: "annotation" } to opt in per component with a "use memo" directive while you evaluate it.

typedRoutes

const nextConfig: NextConfig = {
  typedRoutes: true,
};

Next.js generates types for every route in your app, and next/link's href (plus router.push) gets checked at compile time. A typo like /blgo/my-post becomes a type error instead of a 404 in production. It requires TypeScript, and it pairs well with the setup in using TypeScript with Next.js.

experimental.staleTimes

Controls how long the client-side router cache reuses page segments:

const nextConfig: NextConfig = {
  experimental: {
    staleTimes: {
      dynamic: 30,
      static: 180,
    },
  },
};

By default dynamic pages aren't reused (0 seconds) and static ones are reused for 5 minutes. Raising dynamic makes back-and-forth navigation snappier at the cost of showing slightly older data.

Package Handling

These options control how dependencies are bundled. They fix a surprising number of "works in dev, breaks in build" problems.

serverExternalPackages

By default, the App Router bundles dependencies used in Server Components and Route Handlers. Some packages rely on Node.js internals, native binaries, or dynamic require calls that don't survive bundling. List them here to load them with plain Node.js require:

const nextConfig: NextConfig = {
  serverExternalPackages: ["pdfkit", "canvas"],
};

Next.js already externalizes a list of popular packages automatically, including @prisma/client, sharp, bcrypt, and @aws-sdk/client-s3. Only add packages that cause errors.

transpilePackages

The opposite case: a dependency in node_modules ships raw TypeScript, JSX, or syntax that needs compiling. Listing it makes Next.js compile it:

const nextConfig: NextConfig = {
  transpilePackages: ["@acme/design-tokens"],
};

With Turbopack (the default bundler in v16), workspace packages in a monorepo are transpiled automatically, so you often don't need this for internal packages. It still matters for published packages that ship untranspiled source.

experimental.optimizePackageImports

Some libraries export thousands of modules from a single entry point. This option makes Next.js load only the modules you actually import:

const nextConfig: NextConfig = {
  experimental: {
    optimizePackageImports: ["@acme/icons", "my-utils"],
  },
};

lucide-react, date-fns, lodash-es, @mui/material, @heroicons/react, recharts, and several others are already optimized by default. Add your own large barrel-file packages when the bundle analyzer shows them pulling in more than you use.

turbopack

Turbopack is the default for both next dev and next build in v16. Its config lives at the top level (not under experimental):

const nextConfig: NextConfig = {
  turbopack: {
    rules: {
      "*.svg": {
        loaders: ["@svgr/webpack"],
        as: "*.js",
      },
    },
    resolveAlias: {
      underscore: "lodash",
    },
  },
};

rules runs webpack-compatible loaders, resolveAlias swaps one import for another, and root sets the project root when Turbopack can't infer it (common in monorepos or with linked packages). If you have a custom webpack function in your config (or a plugin adds one) and run a plain next build, the build fails on purpose to avoid silently ignoring it. Either migrate those customizations to turbopack, or keep webpack for now with next build --webpack.

TypeScript and Linting

const nextConfig: NextConfig = {
  typescript: {
    ignoreBuildErrors: false,
    tsconfigPath: "tsconfig.build.json",
  },
};

next build fails on type errors by default. ignoreBuildErrors: true skips the check entirely; only do that if a separate CI step runs tsc --noEmit. tsconfigPath lets you use a different config for builds.

Linting is no longer part of Next.js config. In v16 the next lint command and the eslint key in next.config were removed, and next build no longer lints. Run ESLint (or Biome) directly as its own script and CI step.

Security-Related Options

A few small settings worth reviewing on every project:

const nextConfig: NextConfig = {
  poweredByHeader: false,
  productionBrowserSourceMaps: false,
  allowedDevOrigins: ["dev.local.example.com"],
  experimental: {
    serverActions: {
      bodySizeLimit: "2mb",
      allowedOrigins: ["app.example.com", "*.example.com"],
    },
  },
};
  • poweredByHeader: false removes the X-Powered-By: Next.js header. It won't stop a determined attacker, but there's no reason to advertise your stack.
  • productionBrowserSourceMaps is off by default. Turning it on publishes your original source to anyone who opens DevTools. If you want readable stack traces in an error tracker, upload source maps privately instead.
  • allowedDevOrigins lets you access the dev server from a hostname other than localhost, such as a tunnel or a custom local domain. Cross-origin requests to dev-only endpoints are blocked by default.
  • serverActions.bodySizeLimit raises the default 1 MB limit for Server Action payloads (file uploads, mostly). serverActions.allowedOrigins adds extra origins allowed to invoke actions, which you need behind some reverse proxies where the Origin and Host headers differ.

Also note reactStrictMode is already true by default for the App Router, so you don't need to set it unless you're turning it off.

Developer Experience

logging

const nextConfig: NextConfig = {
  logging: {
    fetches: {
      fullUrl: true,
      hmrRefreshes: true,
    },
  },
};

During development, this prints every fetch from Server Components with its full URL and whether it was a cache hit. It's the fastest way to see why a page isn't using cached data. Server Function calls are logged by default; set logging.serverFunctions: false to quiet them.

devIndicators

The small route indicator in the corner of the dev overlay shows whether the current route is static or dynamic. Move it with devIndicators: { position: "bottom-right" }, or hide it with devIndicators: false. Errors still show either way.

Removed and Legacy Options

If you're upgrading an older config, watch for these:

  • serverRuntimeConfig and publicRuntimeConfig: removed in v16. Use environment variables, read at request time on the server. The post on build-time vs runtime environment variables shows the replacement pattern.
  • eslint: removed along with next lint.
  • images.domains: deprecated; use remotePatterns.
  • experimental.ppr, experimental.dynamicIO, experimental.useCache: replaced by cacheComponents.
  • env: still works but always inlines values into the client bundle. Prefer .env files and the NEXT_PUBLIC_ prefix.

A Sensible Starting Config

Putting the commonly useful pieces together:

// next.config.ts
import type { NextConfig } from "next";

const nextConfig: NextConfig = {
  poweredByHeader: false,
  typedRoutes: true,
  images: {
    remotePatterns: [new URL("https://cdn.example.com/**")],
    formats: ["image/avif", "image/webp"],
  },
  async headers() {
    return [
      {
        source: "/:path*",
        headers: [
          { key: "X-Content-Type-Options", value: "nosniff" },
          { key: "Referrer-Policy", value: "strict-origin-when-cross-origin" },
        ],
      },
    ];
  },
  async redirects() {
    return [{ source: "/home", destination: "/", permanent: true }];
  },
};

export default nextConfig;

Add output: "standalone" if you self-host in containers, cacheComponents: true once you've adopted the new caching model, and reactCompiler: true once you've tested it against your components.

Conclusion

You don't need to memorize every key in next.config.ts. Know the routing trio (redirects, rewrites, headers) for URL changes, images for remote sources and the stricter v16 defaults, output for how you deploy, cacheComponents and cacheLife for the new caching model, and serverExternalPackages and transpilePackages for the occasional dependency that won't bundle. Keep the config small, type it with NextConfig, and remove options you no longer need. A short config is easier to upgrade.

Tags :
Share :

Related Posts

Adding JSON-LD Structured Data to Next.js Pages for Rich Search Results

Adding JSON-LD Structured Data to Next.js Pages for Rich Search Results

Search engines are good at reading pages, but they still guess. Is "4.7" a rating or a version number? Is that date when the article was published or

Continue Reading
Adding Page Transitions and Animations to Next.js with Framer Motion

Adding Page Transitions and Animations to Next.js with Framer Motion

Animation is one of the easiest ways to make an app feel polished, and one of the easiest ways to make it feel slow. A subtle fade when a page loads,

Continue Reading
Adding Syntax Highlighting to Code Blocks in a Next.js Blog

Adding Syntax Highlighting to Code Blocks in a Next.js Blog

If you write about code, your code blocks are half the post. Plain monospace text in a gray box works, but readers scan code by color: keywords, stri

Continue Reading