vercel/next.js
github.com/vercel/next.js ↗ · 4000 files · 10.7 MB · scan capped
Next.js Getting Started Used by some of the world's largest companies, Next.js enables you to create full-stack web applications by extending the latest React features, and integrating powerful Rust-based JavaScript tooling for the fastest builds.
Overview
This is the `vercel/next.js` monorepo at canary `16.4.0-canary.38` — the Next.js React framework itself, plus the Rust Turbopack bundler (vendored as a git subtree under `turbopack/`), the SWC-based native transform crates under `crates/`, and ~20 sibling npm packages (create-next-app, eslint-plugin-next, next-codemod, font, third-parties, next-rspack). The published `next` package is a dual-runtime system: a Node.js router/render server plus an edge-runtime variant, driven by a Commander-based CLI (`next dev|build|start|typegen|upgrade|experimental-test|…`) and fed by either Turbopack (default) or webpack/rspack. What is distinctive is the sheer test-matrix machinery: every integration test runs across three modes (dev/start/deploy) × three bundlers (turbo/webpack/rspack) × an experimental `cacheComponents` axis, orchestrated by `scripts/run-jest.sh` and a custom `@gate` pragma transformer. It is a framework maintained for millions of downstream apps, so nearly every subsystem is an extension point with a stability contract.
System map
A pnpm + Turborepo + Lerna monorepo (`pnpm-workspace.yaml` globs `apps/*`, `packages/*`, `bench/*`, `crates/*/js`, `turbopack/crates/*/js`) layered on top of a single Cargo workspace (`Cargo.toml`) that builds Turbopack, the next-swc transforms, and the napi bindings. The JS entrypoint is `packages/next/src/bin/next.ts`, a Commander program that lazily dynamic-imports one `src/cli/next-*.ts` module per subcommand. At runtime `next dev` forks a child process running `server/lib/start-server.ts`, which boots `server/lib/router-server.ts` (routing, middleware, dev bundler) which in turn lazily loads `server/lib/render-server.ts` → `NextNodeServer` (`server/next-server.ts`) extending the abstract `BaseServer` (`server/base-server.ts`). Storage is the filesystem: `.next/` manifests plus a pluggable `IncrementalCache` / `CacheHandler` layer. Turbopack is reached through napi via `packages/next/src/build/swc/index.ts` wrapping the Rust `Project` type in `crates/next-api/src/project.rs`.
- `packages/next/src/bin/next.ts` is the single published binary (`"bin": {"next": "./dist/bin/next"}`) and registers every subcommand on a `NextRootCommand extends Command`: `build`, `experimental-analyze`, `dev` (isDefault), `export` (hidden), `info`, `start`, `telemetry`, `typegen`, `upgrade`, `experimental-test`, `experimental-request-insights`, and an `internal` group with `trace`, `query-trace`, `post-build`, `upload-trace`, `static-routes-info`, `agent-feedback-instructions`.
- Each subcommand is code-split behind a dynamic import inside `.action()` (e.g. `import('../cli/next-export.js').then((mod) => mod.nextExport())`), so CLI startup never loads the build or dev-server graph unless that command is invoked.
- `next dev` runs as two processes: the CLI process supervises, and `fork(require.resolve('../server/lib/start-server'))` spawns the actual server with `NEXT_PRIVATE_WORKER=1`, `__NEXT_DEV_SERVER='1'`, `NEXT_PRIVATE_TRACE_ID`, and a `WATCHPACK_WATCHER_LIMIT=20` workaround on darwin; options cross the boundary over IPC messages `nextWorkerReady` → `nextWorkerOptions` → `nextServerReady`.
- `startServer()` in `server/lib/start-server.ts` creates the raw `http`/`https` server (https only when `--experimental-https` supplies a self-signed cert) and installs a `requestListener` that awaits a `handlersPromise` before delegating to the router-server handler, so the socket is listening before the bundler is ready.
- `server/lib/router-server.ts#initialize()` is the real runtime boundary: it builds the filesystem checker, optionally instantiates `setupDevBundler` + `DevBundlerService` in dev, wires `renderServer.instance = require('./render-server')`, and returns `{ requestHandler, upgradeHandler }` used by both `next dev` and `next start`.
- `BaseServer` (3459 lines) is the abstract render core; its `protected abstract` surface — `findPageComponents`, `getPagesManifest`, `getPrerenderManifest`, `renderHTML`, `getIncrementalCache`, `handleUpgrade`, `getMiddleware` — is implemented by `NextNodeServer` for Node and by a separate web/edge server under `packages/next/src/server/web/`.
- Build is one giant traced function: `build()` in `packages/next/src/build/index.ts` (4815 lines) wraps everything in a `trace('next-build')` span, loads config, runs `discoverRoutes`/`createPagesMapping`, then branches to either `turbopackBuild(telemetry)` or `webpackBuild(useBuildWorker, [...])`, and finally writes `ROUTES_MANIFEST`, `PRERENDER_MANIFEST`, `APP_PATH_ROUTES_MANIFEST` and friends via a shared `writeManifest<T>()` helper.
- Turbopack is consumed through napi: `packages/next/src/build/swc/index.ts` (1905 lines) defines a `ProjectImpl implements Project` class that forwards to native calls like `binding.projectEntrypointsSubscribe`, `binding.projectWriteAllEntrypointsToDisk`, `binding.projectClientHmrEvents`, and `binding.projectGetSourceMap`, all backed by `crates/next-api/src/project.rs`'s `ProjectContainer`/`Project` turbo-tasks types.
- The Rust workspace excludes `rspack/crates/binding` and tunes profiles aggressively — `turbo-persistence` gets `opt-level = 1` even in dev, release uses `lto = "thin"` — indicating the persistence layer and cold-build time are known hot spots.
- Storage is layered: `IncrementalCache` (`server/lib/incremental-cache/index.ts`) picks a handler from `globalThis[Symbol.for('@next/cache-handlers')].FetchCache` or falls back to `FileSystemCache`, which writes under `serverDistDir/../cache/fetch-cache`, `serverDistDir/pages`, and `serverDistDir/app`.
- Build/test commands are defined in the root `package.json`: `pnpm build` (`turbo run build`), `pnpm build-all` (adds `build-native-auto`), `pnpm --filter=next dev` for the taskr watch build, `pnpm test-unit`, and a 20-entry matrix of `test-{dev,start,deploy}-{webpack,turbo,rspack}[-experimental]` scripts that all shell into `scripts/run-jest.sh`.
- Turborepo caches `build`/`dev` on `dist/**` outputs and declares a `pack-for-isolated-tests` task producing `packed.tgz`, which is how integration tests install a real tarball of `next` rather than a symlinked workspace copy.
Feature catalog
User-facing surface splits four ways: the `next` CLI subcommands, the runtime request-handling features (App Router, Pages Router, middleware/proxy, server actions, image optimization, RSC prefetching), the sibling npm packages (create-next-app, eslint-plugin-next, codemods, fonts, third-parties, bundle-analyzer UI), and a dev-time developer-experience layer (dev overlay, MCP server, request insights, typegen). Nearly every experimental capability is gated behind a key in `ExperimentalConfig` and mirrored into bundles by `define-env.ts`. Documentation is itself a product: 1153 markdown files including `docs/01-app` + `docs/02-pages` and 258 `errors/*.mdx` pages that the dev overlay deep-links into.
- `next dev` supports self-signed HTTPS (`--experimental-https`, `--experimental-https-key/-cert/-ca` via `createSelfSignedCertificate`), Node inspector passthrough with automatic port+1 remapping for the forked child, and CPU profiling through `--experimental-cpu-prof` writing into a `.next-profiles` directory.
- A dev-only MCP server is mounted at `/_next/mcp` by both hot reloaders via `getMcpMiddleware`, using `StreamableHTTPServerTransport` with a 1MB body cap, exposing agent tools `compile-route`, `get-compilation-issues`, `get-errors`, `get-logs`, `get-page-metadata`, `get-project-metadata`, `get-request-insights`, `get-routes`, and `get-server-action-by-id`.
- Request Insights is a dev feature reachable at a dedicated endpoint (`REQUEST_INSIGHTS_DEV_ENDPOINT`) that returns `getRequestInsightsSnapshot()` as JSON, 404s with an explanatory body when `experimental.requestInsights` is off, and is also surfaced as the `next experimental-request-insights` CLI subcommand.
- Image optimization is a first-class route: `NextNodeServer.handleNextImageRequest` validates params, consults an `ImageOptimizerCache`, and calls `imageOptimizer`/`fetchExternalImage`/`fetchInternalImage`, enforcing `images.localPatterns`, `images.remotePatterns`, http/https-only protocols, and an anti-recursion check on `/_next/image`.
- Server Actions ship with built-in CSRF defence: `action-handler.ts` compares the `Origin` header against `x-forwarded-host`/`host`, allows extra hosts via `serverActions.allowedOrigins` through `isCsrfOriginAllowed`, and aborts with `Invalid Server Actions request.` when neither host header is present.
- Both routers are live: `server/route-modules/` carries four families — `app-page`, `app-route`, `pages`, `pages-api` — each a `RouteModule` subclass, with `AppPageRouteModule.render()` and `.prerender()` delegating to `renderToHTMLOrFlight` / `prerenderToHTMLOrFlight`.
- Cache Components / PPR is the headline experimental feature: `cacheComponents` is now a top-level config key (the old `experimental.cacheComponents` and `experimental.ppr` are marked `@deprecated`), and enabling it implicitly turns on `experimental.ppr` and `experimental.cachedNavigations`.
- `"use cache"` is backed by a swappable handler registry keyed on `Symbol.for('@next/cache-handlers')` / `-map` / `-set`, with dev-only extras (`-dev-fronts`, `-dev-tiered`, `-private`) and a `DEV_MEMORY_CACHE_SIZE` of 50MB so `cacheMaxMemorySize: 0` does not make dev reloads uncached.
- i18n domain routing and locale redirects are handled before render, directly in the router-server request handler, using `detectDomainLocale` + `getLocaleRedirect` and short-circuiting with a `307 TemporaryRedirect`.
- `packages/next-codemod` ships 21 named transforms for migrations, including `next-async-request-api`, `middleware-to-proxy`, `metadata-to-viewport-export`, `cra-to-next`, `remove-experimental-ppr`, and `next-lint-to-eslint-cli`, driven by the `next upgrade` subcommand.
- `@next/eslint-plugin-next` exposes 23 rules through a single `plugin.rules` map with two presets — `recommendedRules` (16 warn + 6 error) and `coreWebVitalsRules` which escalates `no-html-link-for-pages` and `no-sync-scripts` to error.
- Telemetry is opt-out: `Telemetry` stores an `anonymousId` and `telemetry.enabled` in a conf file, disables itself on `NEXT_TELEMETRY_DISABLED`, prints the `https://nextjs.org/telemetry` notice, and is toggled by the `next telemetry` subcommand.
Data flow
A request enters at the Node `http.Server` in `start-server.ts`, is immediately sanitised by `filterInternalHeaders()` (which deletes a fixed `INTERNAL_HEADERS` list so clients cannot forge `x-middleware-rewrite`, `x-matched-path`, etc.), then flows through the router-server's `resolveRoutes` pipeline (headers → redirects → middleware → beforeFiles → filesystem → afterFiles → dynamic routes → fallback), and only then crosses into the render server. Inside `BaseServer.handleRequest` the request is wrapped in an OTEL span and per-request metadata is attached via `addRequestMeta`, after which render state is carried in AsyncLocalStorage (`workAsyncStorage`, `workUnitAsyncStorage`) rather than threaded through arguments. Output is a `RenderResult` piped to the socket, with cacheable results passing through `ResponseCache` → `IncrementalCache` → a `CacheHandler`. Build-time data flows the other direction: source files → route discovery → bundler (Turbopack over napi, or webpack) → `.next/*-manifest.json` → read back by `setupFsCheck` at server boot.
- The first thing `requestHandlerImpl` does with a live request is `addRequestMeta(req, 'relativeProjectDir', ...)` and then `filterInternalHeaders(req.headers)` — unless `NEXT_PRIVATE_TEST_HEADERS` is set, which is the documented test-only escape hatch.
- `INTERNAL_HEADERS` is an explicit allow-nothing list of 11 names (`x-middleware-rewrite`, `x-middleware-redirect`, `x-middleware-set-cookie`, `x-middleware-skip`, `x-middleware-override-headers`, `x-middleware-next`, `x-now-route-matches`, `x-matched-path`, `x-nextjs-data`, `x-next-resume-state-length`, `next-resume`); any new internal header must be added here or it is forgeable from outside.
- Routing is a declarative ordered list built once in `resolve-routes.ts`: `middleware_next_data`, `middleware`, `rewrites.beforeFiles`, a second middleware check, `rewrites.afterFiles`, then `rewrites.fallback` — with all rewrite phases skipped entirely when `opts.minimalMode` is set (the deployed/serverless shape).
- Middleware runs out-of-process-shaped: `resolveRoutes` sets `middlewareInvoke: true` request meta, invokes the render server, and then translates the returned `Response` headers — hoisting `x-middleware-override-headers` into real request headers by reading each `x-middleware-request-<key>` and deleting it, and synthesising `x-middleware-refresh` when neither rewrite, next, nor location was set.
- `setupFsCheck()` is the build→runtime data handoff: it reads `routes-manifest.json`, the app/pages routes manifests, and the `public/` + `.next/static/` directory listings into `Set`s (`publicFolderItems`, `nextStaticFolderItems`, `appFiles`, `pageFiles`) plus a sorted `dynamicRoutes` array and a `middlewareMatcher`, then exposes a single LRU-backed `getItem(pathname)`.
- Per-request state is passed via `addRequestMeta`/`getRequestMeta` keys (`invokePath`, `invokeQuery`, `middlewareInvoke`, `locale`, `isRSCRequest`, `relativeProjectDir`) rather than function parameters, which is what lets the router server and render server share a request object across the lazy-init boundary.
- Render-time state lives in AsyncLocalStorage: `app-render.tsx` repeatedly does `workUnitAsyncStorage.run(...)` to produce the RSC payload and the Flight stream, and `workAsyncStorage` carries the work store; there is no explicit context object threaded through the React tree.
- Cache Components prerendering is a two-pass flow: a prospective prerender fills a `PrerenderResumeDataCache` created by `createPrerenderResumeDataCache()`, that cache is attached to `requestStore.resumeDataCache`, and a second 'final'/runtime prerender reuses the filled cache — the comments state caches must be shared between the prospective and final prerender.
- Cacheable responses flow value-as-stream: a `CacheEntry.value` is a `ReadableStream<Uint8Array>` with a documented contract that handlers must return a new stream on every `get` and must not retain the stream after `set` resolves, because a retained stream pins the originating request's async context.
- Tag-based revalidation is a side-channel: `FileSystemCache` mutates a module-scoped `tagsManifest` map with `{ expired: now }` entries and reads it back through `areTagsExpired`, so tag state is per-process in the filesystem handler.
- Build-time config is projected into user bundles as `process.env.__NEXT_*` string literals by `getDefineEnv()` — 40+ keys including `__NEXT_BUNDLER`, `__NEXT_DEV_SERVER`, `__NEXT_CACHE_COMPONENTS`, `__NEXT_USE_CACHE`, `__NEXT_IMAGE_OPTS`, and the client router bloom filters `__NEXT_CLIENT_ROUTER_S_FILTER`/`_D_FILTER`.
- `process.env.__NEXT_USE_NODE_STREAMS` is set to `false` for the edge server and `true` otherwise, which is the fork point for which stream implementation the render path compiles against.
Behavior traces
Three flows dominate the codebase. (1) Dev startup: CLI parse → fork → HTTP listen → router-server init → dev bundler → render server lazy init. (2) A production request: socket → header filtering → route resolution → `BaseServer.handleRequest` → route module render → response cache → pipe. (3) A production build: config load → route discovery → bundler compile → static generation → manifests → trace collection → adapter callback. Each is traceable through named functions, and each is instrumented with a `trace()`/OTEL span so the sequence is visible in `.next/trace`.
- Dev startup order: `bin/next.ts` checks the Node version against `__NEXT_REQUIRED_NODE_VERSION_RANGE` and exits 1 on mismatch, warns if `react`/`react-dom` fail `require.resolve`, stamps `NEXT_PRIVATE_START_TIME`, then runs the `dev` action.
- `nextDev` then opens a `trace('next-dev')` session span, records a 'rage restart' if the previous session for this project dir stopped within `RAGE_RESTART_THRESHOLD_MS` (90s) using a shared `dev-state.json` keyed by project directory under the OS cache dir, and forks the child.
- In the child, `startServer()` sets `process.title = 'next-server (v...)'`, creates the http(s) server with a `requestListener` that awaits `handlersPromise`, registers an `'upgrade'` handler that destroys the socket on error, and only afterwards calls `initialize()` to populate `requestHandler`/`upgradeHandler`.
- `initialize()` runs `setupDevBundler({ ... renderServer ... })` inside a `setupDevBundlerSpan`, wraps the result in `new DevBundlerService(...)`, assigns `renderServer.instance = require('./render-server')`, builds `renderServerOpts`, sets `renderServerOpts.serverFields.routerServerHandler = requestHandlerImpl` (a deliberate back-reference), and awaits `renderServer.instance.initialize(renderServerOpts)`.
- Only once handlers exist does the child `process.send({ nextWorkerReady: true })`; the parent replies with `nextWorkerOptions`, the child later sends `{ nextServerReady: true, port, distDir }`, and the parent stores `port` (for restart reuse) and `distDir` (for telemetry and trace upload).
- Dev restart is signalled by exit code: `requestListener`'s `finally` block calls `getMemoryRestartStats()` and, when near the heap limit, emits a `server-restart-close-to-memory-threshold` span, flushes traces and `process.exit(RESTART_EXIT_CODE)`; the parent sees that code, uploads the existing `.next/trace` before it is overwritten, and restarts.
- Request trace (dev): `requestHandlerImpl` → `filterInternalHeaders` → request-insights endpoint check → i18n `getLocaleRedirect` → optional `compress()` with a `res.once('close')` hook calling `releaseCompressionStream` to avoid a zlib leak on client disconnect → `handleRequest(0)`, which first calls `blockCrossSiteDEV`.
- `handleRequest(handleIndex)` is recursive with a hard stop — it throws `Attempted to handle request too many times` once `handleIndex > 5` — and `invokeRender` re-enters it at `handleIndex + 1` when the render server throws `NoFallbackError`.
- Render trace: `BaseServer.handleRequest` → `await this.prepare()` → `tracer.withPropagatedContext(req.headers, ...)` → `tracer.trace(BaseServerSpan.handleRequest, ...)` → `handleRequestImpl`, with the span renamed in `.finally()` to `RSC GET /route` or `GET /route` from `next.route`, and `http.route` propagated up to a platform-created parent span for APM tools.
- When request insights are enabled the whole handler is additionally wrapped in `runWithRequestInsightsIdentity({ requestId, htmlRequestId, url }, handleRequest)`, taking the id from the `NEXT_REQUEST_ID_HEADER`/`NEXT_HTML_REQUEST_ID_HEADER` headers or minting one with `nanoid()`.
- `handleRequestImpl` normalises before routing: it calls `patchSetHeaderWithCookieSupport` so middleware-set cookies survive API routes/`getServerSideProps`, then 308-redirects any URL matching `/(\\|\/\/)/` through `normalizeRepeatedSlashes`.
- Page resolution: `NextNodeServer.findPageComponents` → `loadComponents({...})` reads the pages manifest and returns a `ComponentMod`; the corresponding `RouteModule` subclass then runs, e.g. `AppPageRouteModule.render()` → `renderToHTMLOrFlight()`.
Testing signals
Jest 29 is the single test runner for JS, configured through `next/jest` itself (dogfooding) in the root `jest.config.js`, with `rootDir: 'test'` plus extra roots into `packages/next/src`, `packages/next-codemod`, `packages/font/src`, `packages/next-routing`, and `packages/eslint-plugin-internal`. Integration coverage is very large — roughly 1183 test files in `test/e2e`, 375 in `test/production`, 314 in `test/development`, 116 in `test/unit` — and every one of them runs under a mode × bundler × experimental-axis matrix selected by env vars in `scripts/run-jest.sh`. Rust has its own lane (`test-cargo-unit`, `test-cargo-unit-miri`, `rust-check`, `rustdoc-check`) in CI. The notable gap is that the harness itself, the CI orchestration in `run-tests.js`, and the deploy mode are lightly covered by anything other than running them.
- `jest.config.js` is built from `nextJest()` — the framework tests itself through its own `next/jest` transformer — and then wrapped by `withGateTransformer(await createConfig())` so the `@gate` pragma rewrite chains in front of next/jest's SWC transform rather than replacing it.
- The test matrix is expressed purely as environment variables: `scripts/run-jest.sh` maps `--mode` to `NEXT_TEST_MODE`, `--bundler` to `IS_WEBPACK_TEST` / `IS_TURBOPACK_TEST` / `NEXT_RSPACK`+`NEXT_TEST_USE_RSPACK`, `--experimental` to `__NEXT_CACHE_COMPONENTS=true`, and `--headless` to `HEADLESS=true`, then `exec jest --runInBand`.
- `__NEXT_TEST_AXIS` implements a lettered flag-matrix dimension: setting the axis implies `__NEXT_CACHE_COMPONENTS=true` and vice-versa, and a fixture opts in by keying a flag on it (`concurrentRouterQueue: process.env.__NEXT_TEST_AXIS !== 'A'`) paired with a `// @gate concurrentRouterQueue` on affected tests.
- The `@gate` registry has two tiers: `staticCondition` (run mode, bundler, React version — known at collection time and the only kind `@force-gate` accepts) and `lazyCondition` (a predicate over the fixture's *resolved* `next.config` read inside the test body); an undeclared condition name fails the whole suite at collection time, so typos cannot silently disable a gate.
- `nextTestSetup()` in `test/lib/e2e-utils/index.ts` is the mandated entry point; it internally calls `createNext()` (explicitly documented as not for direct test use), builds a `NextInstance` from one of `NextDevInstance` / `NextStartInstance` / `NextDeployInstance`, and returns a `Proxy<NextInstance>` so the instance can be referenced before it exists.
- Test timeouts are enforced by proxying jest's `it`/`test`: `wrapJestTestFn` injects a 60s `individualTestTimeout` when a test supplies no explicit timeout, while setup hooks get a longer 120s (240s on win32) budget overridable by `NEXT_E2E_TEST_TIMEOUT`.
- `retry()` in `test/lib/next-test-utils.ts` is the sanctioned wait primitive and `check()` still exists at line 844 but is deprecated by project convention — new tests are required to use `retry()` + `expect()`.
- Integration tests install a real tarball rather than symlinking: `NextInstance` calls `createNextInstall(...)` from `test/lib/create-next-install.js`, and `test/lib/pnpm-security-settings.js` copies `blockExoticSubdeps` / `minimumReleaseAge` from `pnpm-workspace.yaml` into the isolated fixture dir.
- Deploy mode shells out: `NextDeployInstance` runs `execa(process.env.NEXT_TEST_DEPLOY_SCRIPT_PATH)` and expects the deployment URL on stdout, with separate logs and cleanup scripts — so `pnpm test-deploy` needs external credentials and cannot run hermetically.
- `run-tests.js` (1164 lines) is the CI orchestrator, not jest: it reads/writes test timings to Vercel KV (`@vercel/kv`, `KV_TIMINGS_KEY = 'test-timings'`), applies `getTestFilter()`, defaults to `DEFAULT_CONCURRENCY = 2` and `DEFAULT_NUM_RETRIES = 2`, and emits `##[group]` markers when `CI` is set.
- Bundler-specific expected-failure lists are checked-in manifests — `test/rspack-build-tests-manifest.json`, `test/rspack-dev-tests-manifest.json`, `test/cache-components-tests-manifest.json`, `test/deploy-tests-manifest.json` — maintained by `test/update-bundler-manifest.js`, so 'known failing under rspack' is data, not a skip.
- JUnit reporting is opt-in via `NEXT_JUNIT_TEST_REPORT` and routes output to a bundler-specific directory (`turbopack-test-junit-report`, `rspack-test-junit-report`, or `test-junit-report`), deliberately not counting retries so flaky-but-passing tests still report as failed.
Risk map
The biggest blast-radius surfaces are the header-filtering allowlist, the dev-server origin guard, the Server Actions CSRF check, the image optimizer's SSRF path, and the PR-adoption script that grants contributor code access to CI secrets. Structurally, `app-render.tsx` (10,768 lines), `build/index.ts` (4,815) and `base-server.ts` (3,459) are single files that concentrate enormous behaviour and are the natural home of regressions. There is also a large amount of implicit coupling via global symbols, `process.env.__NEXT_*` compile-time constants, and an env-var-driven test matrix, all of which make 'works locally' a weak signal unless the exact CI env is mirrored.
- `INTERNAL_HEADERS` is a hardcoded allowlist, not a prefix rule — any new internal header (e.g. a future `x-middleware-*`) that is read by server code but not added to this array is forgeable by an external client, and the repo's own AGENTS.md flags this as a mandatory PR review check.
- `filterInternalHeaders` is skipped entirely when `NEXT_PRIVATE_TEST_HEADERS` is set; anything that leaks that env var into a non-test process disables internal-header sanitisation for the whole server.
- `blockCrossSiteDEV` only guards URLs matching `/_next` or `/__nextjs` and explicitly excludes `/_next/image`, `/_next/static/media`, and `/_next/static/immutable/media` from the check, so those three paths are reachable cross-site in dev by design — a deliberate CORS tradeoff worth re-checking whenever media handling changes.
- The dev MCP server at `/_next/mcp` executes agent tools (`get-logs`, `get-server-action-by-id`, `compile-route`) and is protected only by virtue of sitting under the `/_next` prefix that `blockCrossSiteDEV` covers; it parses request bodies with a 1MB cap and swallows every error into a generic JSON-RPC 500.
- Server Actions CSRF hinges on `x-forwarded-host` being trustworthy: `getIsPossibleServerAction`-adjacent logic prefers `x-forwarded-host` over `host` when comparing to `Origin`, so a misconfigured reverse proxy that lets clients set that header weakens the check.
- SSRF protection in the image optimizer is DNS-resolution-based and opt-out: `fetchExternalImage` resolves the hostname and rejects private IPs via `isPrivateIp`, but `images.dangerouslyAllowLocalIP = true` disables it, and the resolve-then-fetch pattern is inherently TOCTOU against DNS rebinding.
- `matchWildcardDomain` in `csrf-protection.ts` is a hand-rolled glob matcher written specifically because micromatch cannot run at the edge; it carries explicit special-cases (rejecting bare `*`/`**`, rejecting `**` in non-terminal position) and is the shared basis for both `serverActions.allowedOrigins` and `allowedDevOrigins`, so a bug there affects two security checks at once.
- `pnpm pr-adopt` (`scripts/adopt-pr.js`) re-pushes contributor commits into `vercel/next.js`, which grants that code access to repository secrets during install/build/test; it defends with an interactive confirmation naming the author and head SHA, a throwaway worktree, and `core.hooksPath` redirected to an empty directory via `GIT_CONFIG_COUNT` to neutralise `.husky/*` hooks.
- `app-render.tsx` is 10,768 lines and holds the entire RSC render, prospective/final prerender, and resume-data-cache logic in one module — the single highest blast-radius file in the repo.
- Cache handlers are wired through nine `Symbol.for('@next/cache-handlers*')` globals rather than imports; because they live on `globalThis`, two copies of the module share state, and a stale symbol contract between the framework and an external handler package fails silently rather than at type-check time.
- Fragile coupling: `renderServerOpts.serverFields.routerServerHandler = requestHandlerImpl` creates a cycle where the render server calls back into the router server's handler, which is what makes the `handleIndex > 5` recursion guard necessary.
- Deprecated-but-live config surface is a trap: `experimental.ppr`, `experimental.cacheComponents`, and `images.domains` are all marked deprecated yet still read at runtime, so code must handle both the old and new location of the same setting.
Extension points
Next.js is built to be extended at five distinct seams: the CLI command table, the config schema + `define-env` pipeline for feature flags, the `RouteModule` class hierarchy for new route kinds, the `CacheHandler` / adapter interfaces for hosting and storage backends, and the test-harness registries (`@gate` conditions, bundler manifests). The repo also formalises extension of the *agent* workflow itself through `.agents/skills/`, with a meta-skill for authoring new ones.
- New CLI subcommands plug into the Commander table in `bin/next.ts` — add a `.command(name)` + `.action(() => import('../cli/next-<name>.js'))` pair and a matching `Next<Name>Options` type; the `internal` sub-group shows the pattern for hidden/maintenance commands.
- A new feature flag is a four-file change by convention: the type in `server/config-shared.ts` (`ExperimentalConfig`), the zod entry in `server/config-schema.ts`, a default in `defaultConfig`, and a `process.env.__NEXT_*` entry in `build/define-env.ts` when the flag must reach user-bundled code — with the `$flags` skill documenting the runtime-env plumbing needed for pre-compiled internals.
- `RouteModule<D, U, X>` is the abstract base every route kind extends; adding a new kind means a new directory under `server/route-modules/` alongside `app-page`, `app-route`, `pages`, `pages-api`, plus a `RouteKind` entry in `server/route-kind.ts` and a definition in `server/route-definitions/`.
- Hosting platforms extend the build via `NextAdapter`: set `config.adapterPath` (or `NEXT_ADAPTER_PATH`) and export the adapter interface declared in `build/adapter/build-complete.ts`, which receives `AdapterOutput`/`AdapterOutputs` describing every emitted route including edge entries and their `handlerExport`.
- Cache backends plug in two ways: the legacy `CacheHandler` class contract (`get`/`set`/`revalidateTag`) consumed by `IncrementalCache`, and the newer `"use cache"` handler registry populated by `setCacheHandler(kind, handler)` / `initializeCacheHandlers(cacheMaxMemorySize)` with a documented stream-ownership contract.
- A tiered cache composition point already exists (`createTieredCacheHandler` returning a `CacheReadWriteHandler`), with dev-only maps at `Symbol.for('@next/cache-handlers-dev-tiered')` and `-dev-fronts` for layering an in-memory front over a remote handler.
- ESLint rules register in exactly one place: add the rule module under `packages/eslint-plugin-next/src/rules/`, import it in `src/index.ts`, and add entries to `plugin.rules` plus `recommendedRules` (and `coreWebVitalsRules` if it should escalate).
- The `@gate` condition registry in `test/lib/gate/conditions.ts` is a deliberate hand-written extension point — declare a `staticCondition` (available to `@force-gate`) or a `lazyCondition` reading the fixture's resolved config; referencing an undeclared name is a hard collection-time error.
- New test suites are scaffolded, not hand-written: `pnpm new-test --args <appDir> <name> <type>` runs a `turbo gen test` plop generator (types `e2e | production | development | unit`), and `pnpm new-error` generates an `errors/*.mdx` page the same way.
- Codemods extend via `packages/next-codemod/transforms/` with colocated `__testfixtures__` and `__tests__` directories, and are surfaced to users through `next upgrade`.
- Turbopack's Rust side extends through `#[turbo_tasks::function]` methods on `Project` / `ProjectContainer` in `crates/next-api/src/project.rs`, which are then exposed to JS by adding a napi binding and a corresponding method on `ProjectImpl` in `packages/next/src/build/swc/index.ts`.
- Bundled third-party deps extend through `packages/next/taskfile.js`: register `externals['pkg'] = 'next/dist/compiled/pkg'` and add an `ncc_pkg` task, which is what keeps `next` dependency-light at install time.
Contributor handoff
Read `AGENTS.md` first — `CLAUDE.md` is a symlink to it, and it encodes the non-obvious rules an agent will otherwise get wrong (watch-mode-before-editing, mode-specific test scripts, the PR-description policy for fork contributions, the internal-header security review rule). Work in a throwaway git worktree rather than the user's checkout. Expect slow feedback loops: a full `pnpm build-all` is minutes, `pnpm --filter=next build` ~60s, `pnpm --filter=next types` ~10s, and a single e2e file can take minutes because it installs a real tarball. The dominant trap is the env-var matrix: the same test can pass under webpack and fail under Turbopack, or pass plainly and fail under `__NEXT_CACHE_COMPONENTS=true`.
- `CLAUDE.md` is a symlink to `AGENTS.md` (verified via `ls -la`), so editing either edits the same 524-line file — do not treat them as two documents to keep in sync.
- Before editing Next.js source or integration tests, start `pnpm --filter=next dev` (the taskr watch build, ~1-2s per change) in a separate session; skipping it means tests run against a stale `packages/next/dist`, and the project convention is to state explicitly why if you skip it.
- Rebuild rules by change scope: edits under `packages/next/**` after bootstrap → `pnpm --filter=next build`; any Rust/Turbopack change or a branch switch → `pnpm build-all`; type errors only → `pnpm --filter=next types`.
- Always run a test with the script matching the mode AND bundler under verification (`test-dev-turbo`, `test-dev-webpack`, `test-start-turbo`, `test-start-webpack`); never re-run the same suite with different `-t` filters — capture output once with `> /tmp/test-output.log 2>&1` and grep the file.
- Files to read first for any server-side change: `packages/next/src/server/lib/router-server.ts` (request entry), `packages/next/src/server/base-server.ts` (render core contract), `packages/next/src/server/route-modules/route-module.ts` (route kind contract), and `packages/next/src/build/define-env.ts` (what actually reaches the bundle).
- Prefer a throwaway worktree (`git worktree add /tmp/scratch-work <branch>`) and symlink the root `node_modules` into it; expect `TS2307: Cannot find module` for `fast-glob`, `dotenv`, `@playwright/test` as worktree artifacts — those are not regressions and must not be 'fixed'.
- Type-check with the repo's own commands only: `pnpm typescript` runs `tsc --noEmit` against the root `tsconfig.json`, which pulls in `packages/next/types/global.d.ts` augmentations (e.g. `NodeJS.ProcessEnv` requiring `NODE_ENV`); a hand-rolled single-file tsconfig reports clean while CI fails.
- Pre-validate before committing to avoid ~2min lint-staged failures by running exactly what the hook runs: `pnpm prettier --with-node-modules --ignore-path .prettierignore --write <files>` then `npx eslint --config eslint.config.mjs --fix <files>`.
- Hard policy boundary: you may not write full PR descriptions for fork PRs targeting `vercel/next.js`, and you may not create issues/discussions/comments unless the user is verified (via `gh api /user/memberships/orgs`) as a `vercel`/`vercel-labs` org member — offer drafting help and links instead.
- Security review boundary: if a change reads a non-standard request header, check it against `INTERNAL_HEADERS` in `packages/next/src/server/lib/server-ipc/utils.ts` — this is called out in AGENTS.md as a mandatory PR review step, not a suggestion.
- Known trap — `app-page.ts` is a build template compiled by the *user's* bundler, so relative `require()` of internal modules will not resolve in a user project; new helpers must be exported from `entry-base.ts` and reached as `entryBase.*`, and all `react-server-dom-webpack/*` imports must stay in `entry-base.ts`.
- Known trap — keep `require()` behind compile-time `if/else` branches (not early-return/throw) so webpack DCE can eliminate Node-only imports from edge builds, and force Node-gating flags to `false` for edge in `define-env.ts`.
Languages
| Language | Files | Share of source |
|---|---|---|
| TypeScript | 12573 | 48% |
| JavaScript | 11974 | 46% |
| JSON | 1675 | docs/config |
| Markdown | 1313 | docs/config |
| Rust | 1050 | 4% |
| CSS | 761 | docs/config |
| Sass | 216 | 1% |
| YAML | 111 | docs/config |
Start reading here
Probable source entrypoint: Cargo.toml — inferred from the directory tree and manifest, not the upstream docs.
Source roots
crates/ (434 files) packages/ (433 files)
Repository structure
| Top-level path | Files |
|---|---|
crates/ | 434 |
evals/ | 433 |
packages/ | 433 |
turbopack/ | 433 |
examples/ | 433 |
test/ | 433 |
docs/ | 433 |
bench/ | 317 |
Docs found: AGENTS.mdCLAUDE.mdcontributing.mdlicense.mdreadme.md
Package
Cargo.toml — , 0 declared dependencies.
CI & config entrypoints
AGENTS.md— Agent instructionsCLAUDE.md— Claude instructions.github/workflows/automated_code_review.yml— GitHub Actions workflow.github/workflows/build_and_deploy.yml— GitHub Actions workflow.github/workflows/build_and_test.yml— GitHub Actions workflow.github/workflows/build_reusable.yml— GitHub Actions workflow.github/workflows/create_release_branch.yml— GitHub Actions workflow.github/workflows/integration_tests_reusable.yml— GitHub Actions workflow.github/workflows/issue_lock.yml— GitHub Actions workflow.github/workflows/issue_reopen.yml— GitHub Actions workflow
Structural health signals
Average score 7.1/10 across 96 analyzed source files; 24 hotspots surfaced.
scripts/pr-status.js— 1680 lines, score 4.7 (Large file, Deep nesting)run-tests.js— 1164 lines, score 4.7 (Large file, Deep nesting)bench/render-pipeline/benchmark.ts— 1032 lines, score 4.7 (Large file, Deep nesting)
These are deterministic review leads — size, churn, and structural signals — not proof of a bug in this project.
Recent history
2861a364fcbe2026-09-22 — v16.4.0-canary.3895c5e39062e42026-09-22 — feat(turbopack): support `false` values for `resolveAlias` config (#93331)7353da4cc9f72026-09-22 — Enable AI upgrade for canary (#99001)becee66e9b712026-09-22 — fix(turbo-persistence): preserve write errors on writer drop (#98840)cdecbe0953052026-09-22 — Skip slow valued tombstone test under Miri (#98992)