Architecture Overview
Server Runtime
Rendering Pipeline
Client Navigation
Caching and Export
Development Tools
Build and Configuration
Ecosystem Packages
Testing Infrastructure
How It Works
The following files were used as context for generating this wiki page:
Configuration loading in Next.js is responsible for discovering, transpiling, validating, and normalizing project configuration files (next.config.js, next.config.mjs, next.config.ts, and next.config.mts) across different runtime phases. It bridges user-defined settings with internal framework defaults, enforces strict Zod schema validation, and processes advanced features such as custom HTTP routes, experimental flags, and bundler compatibility checks before initializing server processes.
Sources: packages/next/src/server/config.ts:1-60, packages/next/src/server/config-schema.ts:459-460, packages/next/src/shared/lib/constants.ts:109-116
Configuration discovery and loading operates by searching upward from a target directory for recognized configuration files using findUp and executing appropriate module loaders based on file extension and test environment constraints. The process resolves files defined by CONFIG_FILES or custom key lookups, supporting TypeScript transpilation, dynamic ESM imports, and CommonJS requires.
The configuration loading execution path follows a strict sequence from directory traversal to module evaluation and normalization:
findUp(CONFIG_FILES, { cwd: dir }) scans parent directories for configuration filenames starting from the provided working directory dir.basename(path) extracts the specific configFileName when a path match is found.process.env.__NEXT_TEST_MODE === 'jest', require(path) executes because dynamic import() is unsupported inside Jest VM contexts.configFileName === 'next.config.ts', transpileConfig({ nextConfigPath: path, dir }) compiles the TypeScript configuration file.import(pathToFileURL(path).href) dynamically imports the module using URL-escaped file paths.normalizeConfig(phase, interopDefault(userConfigModule)) processes the exported user configuration, executing exported config functions if present.Note
During Jest test execution (process.env.__NEXT_TEST_MODE === 'jest'), dynamic import() calls are bypassed in favor of synchronous require(path) to prevent Node.js VM context incompatibilities.
In addition to primary Next.js server configuration discovery, the utility helper findConfig queries package.json configurations or falls back to known configuration file patterns via findConfigPath.
Warning
Windows absolute paths passed to dynamic ESM imports require explicit file:// protocol prefixing via pathToFileURL() unless executing within Jest worker environments.
Next.js validates finalized configuration objects against comprehensive Zod schemas defined in config-schema.ts. When validation errors occur, diagnostic messages are normalized via normalizeNextConfigZodErrors to separate warning diagnostics from fatal build errors.
The master validation schema configSchema is constructed using z.lazy() around a strict object (z.strictObject) to disallow unexpected top-level properties. Specialized sub-schemas validate routing configurations, custom image loaders, and experimental features.
Caution
Master validation enforces a strict object schema via z.strictObject(). Any unrecognized top-level properties supplied in next.config.js will trigger validation failure issues.
When configuration validation runs, Zod issues are caught and processed by normalizeNextConfigZodErrors to determine whether diagnostics should halt the build or merely log warnings.
The diagnostic normalization call-chain executes as follows:
validateConfigSchema() → normalizeZodErrors(error) → loops through validation issues to inspect issue.path and issue.code → applies targeted deprecation or migration advice → pushes items into fatalErrors or warnings arrays based on shouldExit.
Note
Specific experimental property errors like turbopackPersistentCaching and dynamicIO dynamically rewrite their error messages to instruct developers on newer replacement APIs before forcing a fatal exit.
Once the user configuration file has been discovered and successfully parsed, Next.js performs baseline normalization and combines user-supplied properties with built-in default settings. This phase ensures that every configuration object consumed by downstream compilation and runtime systems contains predictable structure and fallback values even when specific properties are omitted by the developer.
Sources: packages/next/src/server/config-shared.ts:2092-2098, packages/next/src/server/config.ts:15-20
The normalization utility evaluates whether the exported configuration is a static object or a dynamic function exported by the user.
The configuration normalization execution walk-through proceeds as follows:
loadConfig() (or module load) → normalizeConfig(phase, interopDefault(userConfigModule)) → checks if typeof config === 'function' → if functional, executes config(phase, { defaultConfig }) passing the current phase and default settings object → awaits any returned asynchronous promise → returns the finalized configuration object.
Note
If the user export is a function, it receives the execution phase string and an argument bag containing defaultConfig. This allows conditional configuration based on whether Next.js is running in development, production build, or export mode.
The defaultConfig object provides fallback options for experimental properties, server options, and build structures. Specific experimental features incorporate contextual environment helpers such as turbopackFileSystemCacheForBuildDefault() to decide appropriate defaults based on CI environments or stable build flags.
Warning
turbopackFileSystemCacheForBuild evaluates dynamically via turbopackFileSystemCacheForBuildDefault(). It returns false on stable builds unless running inside Vercel CI builder environments where remote caching is guaranteed.
Custom routes and HTTP headers processing is orchestrated via loadCustomRoutes(), which concurrently evaluates user-defined asynchronous configuration functions for headers, rewrites, and redirects. Each category undergoes schema validation, prefix and locale transformation passes via processRoutes(), and aggregation checks to safeguard server performance.
Sources: packages/next/src/lib/load-custom-routes.ts:703-710, packages/next/src/server/config.ts:16-20
When loading user-defined routing rules, execution proceeds through a strict validation and transformation pipeline:
loadCustomRoutes(config) → executes Promise.all([loadHeaders(config), loadRewrites(config), loadRedirects(config)]) concurrently → within each loader (e.g., loadRedirects()), checks if typeof config.redirects === 'function', awaits the returned array, invokes checkCustomRoutes(redirects, 'redirect'), saves raw unedited redirects to config._originalRedirects, applies processRoutes(redirects, config, 'redirect') to inject base paths and i18n locale prefixes, re-runs checkCustomRoutes(), and returns the finalized rules.
Sources: packages/next/src/lib/load-custom-routes.ts:585-601, packages/next/src/lib/load-custom-routes.ts:689-710
Warning
If the combined total of custom headers, redirects, and rewrites exceeds 1000 items, loadCustomRoutes() emits a performance warning to the console.
Rewrites support structural categorization into three distinct execution phases, parsed from an object return value containing beforeFiles, afterFiles, and fallback arrays. Asset prefix rewrites are automatically prepended to beforeFiles unless they collide with the configured base path.
Sources: packages/next/src/lib/load-custom-routes.ts:609-630, packages/next/src/lib/load-custom-routes.ts:644-657
Sources: packages/next/src/lib/load-custom-routes.ts:490-499, packages/next/src/lib/load-custom-routes.ts:652-654
Tip
When config.deploymentId and config.experimental.useSkewCookie are both enabled, loadCustomRoutes() automatically prepends a global cookie-setting header rule (__vdpl=...) and matches incoming requests containing the RSC header to coordinate deployment state.
Once the base configuration object is loaded and parsed, Next.js inspects its properties for deprecated options, records configured canary and experimental features, and validates Turbopack configuration compatibility. This lifecycle stage ensures that outdated configuration properties emit actionable migration warnings and that builds running under Turbopack do not inadvertently rely on unsupported Webpack-centric options.
The deprecation inspection workflow is governed by checkDeprecations() and warnOptionHasBeenDeprecated(). When config loading completes, checkDeprecations() examines the raw user configuration for legacy fields.
Adding an entry: checkDeprecations() → calls warnOptionHasBeenDeprecated() for each legacy key → splits nestedPropertyKey by dots to walk the configuration tree → if found is true, invokes Log.warnOnce(reason) and returns hasWarned = true.
Warning
If a Zod schema validation error encounters unrecognized keys under experimental, specific fields like turbopackPersistentCachingForBuild or turbopackPersistentCaching force an immediate fatal exit by pushing the error into fatalErrors and pointing developers to turbopackFileSystemCacheForBuild or turbopackFileSystemCacheForDev.
When builds execute with Turbopack, validateTurboNextConfig() ensures that unsupported options are caught and reported.
Calling validateTurboNextConfig({ dir, configPhase }) executes the following sequence:
loadConfig(configPhase, dir, { rawConfig: true }) loads the un-normalized configuration object.flattenKeys(rawNextConfig) recursively extracts all nested property keys from the object while skipping undefined values.unsupportedTurbopackNextConfigOptions and ensures its value differs from defaultConfig.process.env.TURBOPACK === 'auto', hasWebpackConfig is true, and hasTurboConfig is false, it logs a critical error and terminates the process with process.exit(1).Next.js configures Node.js module resolution during process initialization by installing custom require hooks that route userland webpack requests to the internal bundled webpack instance. The initialization utility loadWebpackHook() ensures that hooks are installed exactly once per process via an installed boolean flag.
When invoked, loadWebpackHook() delegates to addHookAliases() from packages/next/src/server/require-hook.ts, populating hookPropertyMap with precise mappings for webpack packages, source modules, and plugins.
Sources: packages/next/src/server/config-utils.ts:8-144, packages/next/src/server/require-hook.ts:40-44
The require hook overrides mod._resolveFilename to intercept module resolution requests. If a requested identifier matches a key in hookPropertyMap, it is swapped with the resolved internal path before executing originalResolveFilename.call().
Note
mod.prototype.require is also patched to intercept shared runtime requests ending with .shared-runtime, routing them directly to the vendored contexts directory for the Pages router.