cf.completefrontend
Open lab My progress
THE NEXT.JS FIELD GUIDE

A tour of the Next.js repository

Find your way around vercel/next.js at v16.4.3: packages/next/src, the Rust crates, the vendored React, building Next.js, and following dist to src.

By the end, you can
  • 01
    Find where a feature livesName the folder in vercel/next.js that holds the CLI, the server, the build, the browser code and the Rust compilers.
  • 02
    Follow dist back to srcRead a stack trace from node_modules/next/dist, find each frame's file and line in packages/next/src, and tell a compiled file from a bundled runtime.
  • 03
    Read the source while it runsSay which React the App Router runs on, build Next.js yourself from its docs, and stop in Next.js's own code with a debugger.

Who called the tea shop's route?

Every page of the tea shop is answered by code you didn't write: Next.js's own. That code is open source, in one GitHub repository, vercel/next.js (the next package is in packages/next), and every link to its files here points at the tag v16.4.3, the release every example here was built with. A tag is a name git gives one exact version of the files, so the line numbers never move.

The quickest way into a large codebase is to ask it a question while it runs. Here the tea shop has a Route Handler, /where, that answers with the list of Next.js functions that called it. It was built with next build, run with next start, and its answer was recorded.

A Route Handler that prints who called itNext.js
app/where/
app/where/route.ts
import { readFileSync } from "node:fs";import { SourceMap } from "node:module";import { connection } from "next/server"; // One line of a stack trace: "at name (/…/node_modules/next/dist/file.js:row:column)".const FRAME = /at (?:async )?(?:(\S+) \()?(\S+\/node_modules\/next\/(dist\/\S+?\.js)):(\d+):(\d+)/; function fromSource(line: string) {  const frame = FRAME.exec(line);  if (!frame) return [];  const [, name = "(anonymous)", file, distFile, row, column] = frame;  const map = new SourceMap(JSON.parse(readFileSync(`${file}.map`, "utf8")));  const entry = map.findEntry(Number(row) - 1, Number(column) - 1);  if (!("originalSource" in entry)) return [];  const source = entry.originalSource.replace(/^.*?src\//, "packages/next/src/");  return [`${distFile}:${row}  ->  ${source}:${entry.originalLine + 1}  ${name}`];} export async function GET() {  Error.stackTraceLimit = 40;  const stack = new Error().stack ?? "";  await connection();  return new Response(stack.split("\n").flatMap(fromSource).join("\n"));}
Recorded from Next.js 16.4.3

GET /where200

Response headers
vary rsc, next-router-state-tree, next-router-prefetch, next-router-segment-prefetch
content-type text/plain;charset=UTF-8

The body that arrived, as it arrived

dist/compiled/next-server/app-route-turbo.runtime.prod.js:5  ->  packages/next/src/server/route-modules/app-route/module.ts:825  r0.renderToResponse
dist/compiled/next-server/app-route-turbo.runtime.prod.js:5  ->  packages/next/src/server/route-modules/app-route/module.ts:986  (anonymous)
dist/server/lib/trace/tracer.js:196  ->  packages/next/src/server/lib/trace/tracer.ts:404  NextTracerImpl.trace
dist/compiled/next-server/app-route-turbo.runtime.prod.js:5  ->  packages/next/src/server/route-modules/app-route/module.ts:977  (anonymous)
dist/compiled/next-server/app-route-turbo.runtime.prod.js:5  ->  packages/next/src/server/route-modules/app-route/module.ts:951  (anonymous)
dist/compiled/next-server/app-route-turbo.runtime.prod.js:5  ->  packages/next/src/server/route-modules/app-route/module.ts:950  (anonymous)
dist/compiled/next-server/app-route-turbo.runtime.prod.js:5  ->  packages/next/src/server/route-modules/app-route/module.ts:947  r0.executeRequest
dist/compiled/next-server/app-route-turbo.runtime.prod.js:5  ->  packages/next/src/server/route-modules/app-route/module.ts:1098  r0.handle
dist/compiled/next-server/app-route-turbo.runtime.prod.js:1  ->  packages/next/src/server/route-modules/route-module.ts:1339  r0.handleResponse
dist/server/base-server.js:1765  ->  packages/next/src/server/base-server.ts:2985  NextNodeServer.renderToResponseWithComponentsImpl
dist/server/base-server.js:1824  ->  packages/next/src/server/base-server.ts:3062  NextNodeServer.renderPageComponent
dist/server/base-server.js:1867  ->  packages/next/src/server/base-server.ts:3142  NextNodeServer.renderToResponseImpl
dist/server/base-server.js:1286  ->  packages/next/src/server/base-server.ts:2209  NextNodeServer.pipeImpl
dist/server/next-server.js:340  ->  packages/next/src/server/next-server.ts:1271  NextNodeServer.handleCatchallRenderRequest
dist/server/base-server.js:1008  ->  packages/next/src/server/base-server.ts:1703  NextNodeServer.handleRequestImpl
dist/server/lib/router-server.js:363  ->  packages/next/src/server/lib/router-server.ts:535  invokeRender
dist/server/lib/router-server.js:567  ->  packages/next/src/server/lib/router-server.ts:813  handleRequest
dist/server/lib/router-server.js:636  ->  packages/next/src/server/lib/router-server.ts:923  requestHandlerImpl
dist/server/lib/start-server.js:210  ->  packages/next/src/server/lib/start-server.ts:254  Server.requestListener

Start at the bottom of app/where/route.ts. Line 21 makes an Error only to read its stack: the list of functions that were running when it was made, one per line, the newest first (The stack trace API explains the format). Line 20 raises how many lines it keeps from V8's default of 10 to 40. Line 22 waits for connection(), which tells Next.js to run this handler for each request instead of once during the build. Line 23 answers with the stack, each line rewritten by fromSource.

fromSource, on lines 8 to 17, keeps only the lines in node_modules/next/dist, the folder npm installs. Next.js ships a source map beside each of the server files this stack names: a .map file that says which line of the original source each compiled line came from. (Not every file in dist has one: much of the third-party code in dist/compiled doesn't.) Line 12 reads the map, and line 13 asks it about one line and column (maps count from 0, so both are 1 less). Line 15 shortens the answer to a path in the repository.

Open the Network tab. Each line reads file in dist → file and line in the repository, then the function's name. Read it from the bottom up, in the order the calls happened:

  • start-server.ts:254: Node's HTTP server got the request and called Next.js's requestListener.
  • router-server.ts, three lines: the router server decided what answers this address.
  • next-server.ts and base-server.ts: the server class that renders a route.
  • route-modules/app-route/module.ts: the code that runs a Route Handler. Line 825 calls your GET.

Every one of those files is in one folder of the repository, packages/next/src/server, and From next dev to a response reads them in order. First, the map: where each part of Next.js lives, how the files npm installs come from it, and how to read it while it runs.

Two editor windows side by side. Left, packages/next/src, with the folders cli, server, build, client, shared, lib and compiled. An arrow goes down to a card: the npm package next, its dist folder. Right, the Rust crates: crates/next-core, crates/next-custom-transforms, crates/next-napi-bindings, turbopack/crates/turbo-tasks and the other turbopack crates. An arrow goes down to a card: the npm package @next/swc for your computer.TypeScriptcliserverbuildclientsharedlibcompiled (React, …)Rustcrates/next-corecrates/next-custom-transformscrates/next-napi-bindingsturbopack/crates/turbo-tasksturbopack/crates/turbopack-*npm: next (dist/)npm: @next/swc-<platform>compiled by SWC, file by filecompiled to one .node file
One repository, two languages. The TypeScript in packages/next/src is compiled into the next package's dist folder. The Rust crates, with SWC and Turbopack, are compiled into one native file, published as @next/swc-<platform>.
Real-life analogyA big hotel and its floor plan

A guest sees the reception and the restaurant. The boiler room in the basement keeps everything warm, and few guests ever go there. The floor plan by the lift says which floor holds what. And the bill at the end lists every desk that served the guest, in order, so you can walk back to each one.

In real life: The hotel
In Next.js: The repository vercel/next.js
In real life: The rooms guests see: reception, the restaurant
In Next.js: packages/next/src: the code npm installs as next
In real life: The boiler room in the basement
In Next.js: The Rust crates: SWC and Turbopack
In real life: The floor plan by the lift
In Next.js: A map of the repository: which folder holds what
In real life: A guest's bill, which lists every desk that served them
In Next.js: The stack trace from /where
The main idea

Next.js is one repository with two halves. The TypeScript in packages/next/src becomes the next package you install, and the .map files beside its compiled server code lead from a line in node_modules/next/dist back to the source. The Rust crates become one native file for your computer, with the compilers inside.

packages/next/src, folder by folder

The repository holds about twenty packages in packages, like create-next-app, eslint-config-next and next-codemod. The one you install as next is packages/next, and its source is in packages/next/src. Each row is one of its folders:

The main folders of packages/next/src
FolderWhat it holdsOne file to start with
binThe next command itself: bin/next.ts reads the command line and loads one command.bin/next.ts: the list of commands.
cliOne file per command: next-dev.ts, next-build.ts, next-start.ts and the others.cli/next-dev.ts: starts the server process and passes it --inspect.
serverEverything that answers requests: starting the HTTP server, routing, rendering, caching.server/lib/start-server.ts: the HTTP server, at the bottom of every stack above.
buildnext build: collecting routes, compiling, prerendering, writing .next, and loading the Rust compiler.build/swc/index.ts: loads @next/swc.
clientThe code that runs in the browser: hydration, the App Router's router, Link.client/app-index.tsx: hydrates the first HTML.
sharedCode both sides use, like the helpers that turn a path into a route.shared/lib/router/utils/app-paths.ts: turns a page's path into its route.
libHelpers the CLI, the build and the server share, like the file names proxy and middleware in lib/constants.ts.lib/needs-experimental-react.ts: decides which React.
compiledOther projects' code, copied in or bundled so Next.js controls the exact version: React, commander, webpack and more.compiled/react: the React the App Router runs.

A command starts in bin. The next package's package.json says, in lines 81 to 83, that the command next is the file ./dist/bin/next. That file is compiled from src/bin/next.ts, which lists every command and loads its file from cli only when you run it: next dev loads cli/next-dev.ts on lines 481 and 482, and next start loads cli/next-start.ts on lines 560 and 561.

An import from next/… in your code takes the same kind of path. The where route imports connection from next/server, and this is how that name reaches the source:

From an import to the source, one step per lineText
import { connection } from "next/server"
node_modules/next/server.js, line 15
node_modules/next/dist/server/request/connection.js
packages/next/src/server/request/connection.ts

Line 1 is the import. Line 2 is the small file at the top of the package, server.js, whose line 15 loads connection from dist. Line 3 is that compiled file, and line 4 the TypeScript it came from, connection.ts. So for any next/… import, open the matching file at the top of packages/next, and it names the folder to read.

From src to node_modules/next/dist

The stack had two kinds of files in dist. Files like dist/server/base-server.js match one source file each. But eight of its lines were in dist/compiled/next-server/app-route-turbo.runtime.prod.js, and they led back to two different files. The build of Next.js itself makes both kinds.

One file in, one file out

Most of src is compiled one file at a time by SWC, the Rust compiler Next.js also uses for your code. The repository's build runs tasks written in taskfile.js; this is the one for src/server:

The server task, from taskfile.js at v16.4.3JavaScript
export async function server(task, opts) {
  await task
    .source('src/server/**/!(*.test).+(js|ts|tsx)')
    .swc('server', { dev: opts.dev })
    .target('dist/server')
}

Line 3 picks every .js, .ts and .tsx file under src/server, except tests. Line 4 compiles each one with SWC, and line 5 writes the result into dist/server, in the same place. So src/server/base-server.ts becomes dist/server/base-server.js. The line numbers change, because the types are removed and the imports rewritten: line 1703 of the source is line 1008 of the result. That's why the .map file matters. In lines 126 to 130 of taskfile-swc.js, SWC is asked for a source map that also holds the source's full text (inlineSourcesContent), so a debugger can show the TypeScript even though npm never installs it. The code is in lines 2513 to 2518 of taskfile.js.

The route runtimes: many files in, one file out

The code that runs your routes is different. It's bundled: many source files, React included, are joined into one file per kind of route, by webpack, with the settings in next-runtime.webpack-config.js. This is how those files are named:

The runtime bundles' names, from next-runtime.webpack-config.js at v16.4.3JavaScript
output: {
  path: path.join(__dirname, 'dist/compiled/next-server'),
  filename: `[name]${turbo ? '-turbo' : ''}${
    experimental ? '-experimental' : ''
  }.runtime.${dev ? 'dev' : 'prod'}.js`,

Line 2 puts them in dist/compiled/next-server. Lines 3 to 5 build each name from parts: the kind of bundle (like app-page, app-route or pages), -turbo for apps built with Turbopack, -experimental for experimental React, then dev or prod. That's app-route-turbo.runtime.prod.js: a Route Handler, a Turbopack app, stable React, production. When a route loads, a small file picks one of them:

Picking a runtime, from app-route/module.compiled.js at v16.4.3JavaScript
if (process.env.NODE_ENV === 'development') {
  if (process.env.TURBOPACK) {
    module.exports = require('next/dist/compiled/next-server/app-route-turbo.runtime.dev.js')
  } else {
    module.exports = require('next/dist/compiled/next-server/app-route.runtime.dev.js')
  }
} else {
  if (process.env.TURBOPACK) {
    module.exports = require('next/dist/compiled/next-server/app-route-turbo.runtime.prod.js')
  } else {
    module.exports = require('next/dist/compiled/next-server/app-route.runtime.prod.js')
  }
}

Line 1 asks whether this is development. Lines 2 and 8 ask whether Turbopack built the app. So under next start, line 9 loads the prod bundle, and under next dev, line 3 loads the dev one. (The lines around this excerpt pick the -experimental bundles instead when experimental React is on.) The file is module.compiled.js, and the names come from lines 213 to 217 of next-runtime.webpack-config.js. Try both below: the same route, served by next start and by next dev.

Playground: which runtime file called the route?
The tea shop, served this wayNext.js
app/where/
app/where/route.ts
import { readFileSync } from "node:fs";import { SourceMap } from "node:module";import { connection } from "next/server"; // One line of a stack trace: "at name (/…/node_modules/next/dist/file.js:row:column)".const FRAME = /at (?:async )?(?:(\S+) \()?(\S+\/node_modules\/next\/(dist\/\S+?\.js)):(\d+):(\d+)/; function fromSource(line: string) {  const frame = FRAME.exec(line);  if (!frame) return [];  const [, name = "(anonymous)", file, distFile, row, column] = frame;  const map = new SourceMap(JSON.parse(readFileSync(`${file}.map`, "utf8")));  const entry = map.findEntry(Number(row) - 1, Number(column) - 1);  if (!("originalSource" in entry)) return [];  const source = entry.originalSource.replace(/^.*?src\//, "packages/next/src/");  return [`${distFile}:${row}  ->  ${source}:${entry.originalLine + 1}  ${name}`];} export async function GET() {  Error.stackTraceLimit = 40;  const stack = new Error().stack ?? "";  await connection();  return new Response(stack.split("\n").flatMap(fromSource).join("\n"));}
Recorded from Next.js 16.4.3

GET /where200

Response headers
vary rsc, next-router-state-tree, next-router-prefetch, next-router-segment-prefetch
content-type text/plain;charset=UTF-8

The body that arrived, as it arrived

dist/compiled/next-server/app-route-turbo.runtime.prod.js:5  ->  packages/next/src/server/route-modules/app-route/module.ts:825  r0.renderToResponse
dist/compiled/next-server/app-route-turbo.runtime.prod.js:5  ->  packages/next/src/server/route-modules/app-route/module.ts:986  (anonymous)
dist/server/lib/trace/tracer.js:196  ->  packages/next/src/server/lib/trace/tracer.ts:404  NextTracerImpl.trace
dist/compiled/next-server/app-route-turbo.runtime.prod.js:5  ->  packages/next/src/server/route-modules/app-route/module.ts:977  (anonymous)
dist/compiled/next-server/app-route-turbo.runtime.prod.js:5  ->  packages/next/src/server/route-modules/app-route/module.ts:951  (anonymous)
dist/compiled/next-server/app-route-turbo.runtime.prod.js:5  ->  packages/next/src/server/route-modules/app-route/module.ts:950  (anonymous)
dist/compiled/next-server/app-route-turbo.runtime.prod.js:5  ->  packages/next/src/server/route-modules/app-route/module.ts:947  r0.executeRequest
dist/compiled/next-server/app-route-turbo.runtime.prod.js:5  ->  packages/next/src/server/route-modules/app-route/module.ts:1098  r0.handle
dist/compiled/next-server/app-route-turbo.runtime.prod.js:1  ->  packages/next/src/server/route-modules/route-module.ts:1339  r0.handleResponse
dist/server/base-server.js:1765  ->  packages/next/src/server/base-server.ts:2985  NextNodeServer.renderToResponseWithComponentsImpl
dist/server/base-server.js:1824  ->  packages/next/src/server/base-server.ts:3062  NextNodeServer.renderPageComponent
dist/server/base-server.js:1867  ->  packages/next/src/server/base-server.ts:3142  NextNodeServer.renderToResponseImpl
dist/server/base-server.js:1286  ->  packages/next/src/server/base-server.ts:2209  NextNodeServer.pipeImpl
dist/server/next-server.js:340  ->  packages/next/src/server/next-server.ts:1271  NextNodeServer.handleCatchallRenderRequest
dist/server/base-server.js:1008  ->  packages/next/src/server/base-server.ts:1703  NextNodeServer.handleRequestImpl
dist/server/lib/router-server.js:363  ->  packages/next/src/server/lib/router-server.ts:535  invokeRender
dist/server/lib/router-server.js:567  ->  packages/next/src/server/lib/router-server.ts:813  handleRequest
dist/server/lib/router-server.js:636  ->  packages/next/src/server/lib/router-server.ts:923  requestHandlerImpl
dist/server/lib/start-server.js:210  ->  packages/next/src/server/lib/start-server.ts:254  Server.requestListener
Try it yourself
next start

Under next start, the Route Handler's code comes from app-route-turbo.runtime.prod.js, the bundle for production. Its names are shortened (r0.handle), but the source map still finds module.ts and the line.

Both versions ran the same files with Next.js 16.4.3: next build and next start, or next dev --inspect. The Network tab shows what /where answered; the Terminal shows what the server printed. Reset goes back to next start.

Under next start, the class is called r0: the production bundle is minified, its names shortened to make it smaller. Under next dev, it's AppRouteRouteModule, and the file ends in .dev.js. The source map gives the same line of module.ts for both, line 1098. The dev version also has three more lines, from next-dev-server.ts and trace.ts: in development, a DevServer class wraps each request first.

Left, a window titled src with four cards: router-server.ts, base-server.ts, app-route/module.ts and compiled/react. Right, a window titled dist with three cards: router-server.js plus .map, base-server.js plus .map, and app-route-turbo.runtime.prod.js. Each of the first two source files has an arrow to its own dist file. module.ts and compiled/react both have arrows to the one runtime file.srcrouter-server.tsbase-server.tsapp-route/module.tscompiled/reactdistrouter-server.js + .mapbase-server.js + .mapapp-route-turbo.runtime.prod.jsSWCbundled by webpack, one file per kind of route
Most of src is compiled one file at a time, with a .map file beside each result. The code that runs a route is bundled: app-route/module.ts, the vendored React and more become one file per kind of route, like app-route-turbo.runtime.prod.js.

One more folder: dist/compiled also holds other projects' code, like commander for the command line, webpack, and React. Next.js keeps its own copy of each, so the version it was tested with is the one that runs. The file names in dist are how this release is built, not a public API; they can change in any release.

The Rust crates: SWC and Turbopack

The other half of the repository is Rust. A Rust package is called a crate, and a workspace is a set of crates built together. The root Cargo.toml, Rust's version of package.json, lists them:

The Rust workspace, from Cargo.toml at v16.4.3Text
members = [
  "scripts/send-trace-to-jaeger",
  "crates/next-napi-bindings",
  "crates/wasm",
  "crates/next-api",
  "crates/next-build-test",
  "crates/next-build",
  "crates/next-code-frame",
  "crates/next-core",
  "crates/next-custom-transforms",
  "crates/next-taskless",
  "turbopack/crates/*",
  "turbopack/crates/*/fuzz",
  "turbopack/xtask",
]

Lines 3 to 11 are Next.js's own crates, in crates. Line 12 adds every crate in turbopack/crates: Turbopack, the bundler that builds your app by default since Next.js 16. SWC itself isn't in the list. It's a separate open-source project, and the crates use it as a dependency, swc_core, in line 217 of Cargo.toml. Each row is one crate this course meets:

The crates, and what each one does
CrateWhat it does
crates/next-napi-bindingsJoins Node.js and Rust: it's built into the .node file that build/swc/index.ts loads.
crates/next-coreTeaches Turbopack about Next.js: the app folder, route entries, and which React each part gets.
crates/next-apiThe project as a whole: its routes, entrypoints and outputs, for next dev and next build.
crates/next-custom-transformsNext.js's own SWC transforms, like the ones for "use client" and Server Actions.
turbopack/crates/turbo-tasksThe engine that remembers every result Turbopack computed, so a change only redoes what depends on it.
turbopack/crates/turbopack-*The bundler: one crate each for JavaScript, CSS, images, resolving modules and more.
swc_core, from crates.ioSWC itself, the Rust compiler for JavaScript and TypeScript. It's a separate project; the repository uses version 79.0.0.

None of this Rust code is compiled on your computer. Line 3's next-napi-bindings is built ahead of time into one .node file per operating system and processor, a native add-on that Node.js can load, and each one is published as its own npm package. The next package lists all of them as optional dependencies, and npm installs only the one that fits. This recording lists what npm install put in the tea shop's node_modules, on the Mac with an Apple chip that made the recording:

What npm installed for the tea shopNext.js
app/
app/page.tsx
export default function Home() {  return <h1>Tea shop</h1>;}
Recorded from Next.js 16.4.3
$ next build▲ Next.js 16.4.3 (Turbopack)- Cache Components enabled- Partial Prefetching enabled   Creating an optimized production build ...✓ Compiled successfully  Running TypeScript ...  Finished TypeScript ...  Collecting page data ...✓ Generating static pages (3/3)  Finalizing page optimization ... Route (app)┌ ○ /└ ○ /_not-found ○  (Static)  prerendered as static content # What node_modules holds: 10658 files, 351.5 MB (deeper folders added up)node_modules/.package-lock.json  15.8 kBnode_modules/@emnapi/runtime/  436.9 kB, 17 filesnode_modules/@img/colour/  73.3 kB, 6 filesnode_modules/@img/sharp-darwin-arm64/  292.2 kB, 5 filesnode_modules/@img/sharp-libvips-darwin-arm64/  18.2 MB, 6 filesnode_modules/@img/sharp-wasm32/  9.3 MB, 7 filesnode_modules/@next/env/  11.9 kB, 4 filesnode_modules/@next/swc-darwin-arm64/  90.5 MB, 3 filesnode_modules/@swc/helpers/  282.3 kB, 438 filesnode_modules/@types/node/  2.3 MB, 69 filesnode_modules/@types/react-dom/  32.9 kB, 17 filesnode_modules/@types/react/  408.1 kB, 16 filesnode_modules/baseline-browser-mapping/  136.7 kB, 9 filesnode_modules/caniuse-lite/  1.5 MB, 841 filesnode_modules/client-only/  611 bytes, 3 filesnode_modules/csstype/  1.3 MB, 5 filesnode_modules/detect-libc/  26.4 kB, 8 filesnode_modules/nanoid/  26.8 kB, 26 filesnode_modules/next/  192.1 MB, 8705 filesnode_modules/picocolors/  6.4 kB, 7 filesnode_modules/postcss/  216.0 kB, 55 filesnode_modules/react-dom/  8.1 MB, 43 filesnode_modules/react/  178.7 kB, 27 filesnode_modules/scheduler/  82.7 kB, 15 filesnode_modules/semver/  101.1 kB, 53 filesnode_modules/sharp/  962.2 kB, 47 filesnode_modules/source-map-js/  142.7 kB, 18 filesnode_modules/styled-jsx/  1.0 MB, 20 filesnode_modules/tslib/  90.4 kB, 14 filesnode_modules/typescript/  23.6 MB, 132 filesnode_modules/undici-types/  83.7 kB, 41 files

In the Terminal tab, after the build, @next/swc-darwin-arm64 is about 90 MB in 3 files: one of them is next-swc.darwin-arm64.node, with SWC and Turbopack inside. next is about 192 MB in thousands of files, almost all of it dist. On Linux or Windows, npm picks another @next/swc-… package. The sizes are this machine's and this version's. When next build starts, loadNative in build/swc/index.ts loads the package for your platform, and loadBindings, in lines 190 to 265 of the same file, falls back to a WebAssembly build of the same crates when there isn't one.

The Rust crates get a lesson of their own, SWC and Turbopack, and the transforms for "use client" and Server Actions in next-custom-transforms get 'use client' and 'use server' under the hood.

Where React comes from

A new Next.js project's package.json lists react and react-dom, version 19.3.0. You might expect every page to run on them. Here is the tea shop with a page that prints version, the React version string React exports, from a Server Component, from a Client Component, and from an old Pages Router page in the same project:

Which React renders each page?Next.js
app/pages/
app/page.tsx
import { version } from "react";import { ClientVersion } from "./client-version"; export default function Home() {  return (    <main>      <h1>Tea shop</h1>      <p>Server Component React: {version}</p>      <ClientVersion />    </main>  );}
Recorded from Next.js 16.4.3
localhost:3000/
Tea shop

Server Component React: 19.3.0-canary-278794d7-20261002

Client Component React: 19.3.0-canary-278794d7-20261002

The page as the browser shows it when its HTML arrives, drawn with plain styles. Its JavaScript doesn't run here.

In app/page.tsx, line 1 imports version from react, and line 8 prints it. The Browser tab shows 19.3.0-canary-278794d7-20261002 for the Server Component and the same for ClientVersion, the Client Component in app/client-version.tsx. Pick /legacy above the page: pages/legacy.tsx printed 19.3.0. The recording adds one more fact: the JavaScript files the home page loads contain the text 19.3.0-canary-278794d7-20261002, so the browser runs that build too.

A canary is a build of React published from its newest code, before a stable release (React's blog post React Canaries explains the channel). Next.js's docs say it in line 44 of docs/index.mdx: the App Router uses React canary releases built in, with all of stable React 19 and newer features that frameworks are trying out. The copy lives in next/dist/compiled/react, and the repository pins it in its root package.json:

The React builds Next.js copies in, four of the lines of the root package.json at v16.4.3JSON
"react-builtin": "npm:react@19.3.0-canary-278794d7-20261002",
"react-dom-builtin": "npm:react-dom@19.3.0-canary-278794d7-20261002",
"react-dom-experimental-builtin": "npm:react-dom@0.0.0-experimental-278794d7-20261002",
"react-experimental-builtin": "npm:react@0.0.0-experimental-278794d7-20261002",

Line 1 names the canary as react-builtin, and line 2 the matching react-dom. Lines 3 and 4 name an experimental build, with features React hasn't settled yet (they're lines 262, 264, 265 and 266 of the file). The build task copy_vendor_react, declared at line 1490 of taskfile.js, copies these packages into src/compiled/react and its neighbors, which are kept in git; lines 1561 to 1569 copy React's files there. The script scripts/sync-react.js moves the pin to a newer canary.

Then the bundler sends App Router code's react imports to that copy. For Turbopack, that happens in Rust, in crates/next-core. First it picks the channel:

Stable or experimental, from next_import_map.rs at v16.4.3Rust
let react_channel = if *next_config.use_react_experimental().await? {
    "-experimental"
} else {
    ""
};

Lines 1 to 5 pick the folder's ending: -experimental when the app needs experimental React, and nothing otherwise, so the stable copy in next/dist/compiled/react is the one used. Then, in lines 905 to 970, the aliases depend on where the code runs. Server code on Node.js goes through entry files of Next.js, like next/dist/server/route-modules/app-page/vendored/rsc/react for Server Components and …/vendored/ssr/react for rendering Client Components to HTML, which hand out that copy; other code, like the Edge runtime's, imports next/dist/compiled/react itself. Either way, it's the canary the page printed. Which settings turn on experimental React is in TypeScript, and the Rust side says it must stay in step with it:

The settings that need experimental React, from needs-experimental-react.ts at v16.4.3TypeScript
export function needsExperimentalReact(config: NextConfig) {
  const { blockingSSR, taint, transitionIndicator, gestureTransition } =
    config.experimental || {}
  return Boolean(
    blockingSSR || taint || transitionIndicator || gestureTransition
  )
}

Lines 2 and 3 read four experimental settings from next.config.ts, and lines 4 to 6 answer true if any is on. The tea shop sets none of them, so it got the canary. The Pages Router is different again:

The Pages Router keeps your react, from next_import_map.rs at v16.4.3Rust
match ty {
    ServerContextType::Pages { .. } | ServerContextType::PagesApi { .. } => {
        import_map.insert_exact_alias(rcstr!("react"), external);
        import_map.insert_wildcard_alias(rcstr!("react/"), external);
        import_map.insert_exact_alias(rcstr!("react-dom"), external);
        import_map.insert_exact_alias(rcstr!("react-dom/client"), external);
        import_map.insert_wildcard_alias(rcstr!("react-dom/"), external);

Line 2 matches server code for pages, and lines 3 to 7 mark react and react-dom as external: not bundled, but loaded from the app's own node_modules when it runs. That's the 19.3.0 that /legacy printed, and why the Pages Router still works with the React version in your package.json (Migrating to the App Router runs both routers side by side).

Left, labeled Pages Router: a card node_modules/react above a card 19.3.0, with an arrow down to a window holding pages/legacy.tsx. Right, labeled App Router: a card next/dist/compiled/react above a card 19.3.0-canary-278794d7-20261002, with an arrow down to a window holding app/page.tsx and app/client-version.tsx.Pages Routernode_modules/react19.3.0pagespages/legacy.tsxApp Routernext/dist/compiled/react19.3.0-canary-278794d7-20261002appapp/page.tsxapp/client-version.tsx
The same import { version } from "react", two answers. In the App Router, Server and Client Components both get the React that Next.js carries in next/dist/compiled/react. A Pages Router page gets the app's own react package.

React's own repository has a tour like this one: A tour of the React repository. The react-server-dom-turbopack and react-server-dom-webpack folders next to compiled/react hold React's code for Server Components, which 'use client' and 'use server' under the hood reads.

Running a local build of Next.js

To change Next.js, or to put a console.log in its source, you build it yourself. These commands are from the repository's own guides, contributing/core/developing.md and building.md. They aren't recorded: a full build takes many minutes and gigabytes of disk.

Building Next.js from its repository (from its contributing guides, not run here)Shell
gh repo clone vercel/next.js -- --filter=blob:none --single-branch
cd next.js
corepack enable pnpm
pnpm install
pnpm build
pnpm dev
pnpm swc-build-native
pnpm next ./dev-app
pnpm pack-next --tar && pnpm unpack-next path/to/project

Each row is one of those commands, in order:

The commands, and what each one does
CommandWhat it does
gh repo clone vercel/next.js -- --filter=blob:none --single-branchClones the repository without downloading every old file version until you need it.
corepack enable pnpm, then pnpm installUses the pnpm version the root package.json names (pnpm@10.33.0) and installs every workspace package.
pnpm buildBuilds every package once: SWC compiles packages/next/src into dist, webpack bundles the route runtimes, and tsc writes the types.
pnpm devWatches packages/next/src and compiles again on every save.
pnpm swc-build-nativeBuilds the Rust code into a new .node file. Only needed when you change Rust code.
pnpm next ./dev-appRuns your local Next.js on a folder inside the repository, with Node's source maps turned on.
pnpm pack-next --tar && pnpm unpack-next path/to/projectPacks your local Next.js into tarballs and installs them into another project.

The repository uses pnpm, a package manager like npm. Its root package.json names pnpm@10.33.0 and Node.js 20.9 or later, and .node-version says v20. Line 5, pnpm build, runs the tasks from the last section: SWC for dist, webpack for the runtime bundles. Line 7 is only for Rust changes and needs Rust installed; without it, the build uses the published @next/swc binary. Line 8 runs next from your build, on a folder inside the repository (developing-using-local-app.md explains it): the root script next sets NODE_OPTIONS="--trace-deprecation --enable-source-maps", so Node.js turns stack traces into source lines for you. The guide says linking the package into another project isn't enough, because Turbopack doesn't follow links outside a project's folder; line 9 packs it into files instead.

A smaller first step

You can read and change Next.js without building it. In any project, edit a file in node_modules/next/dist, add a console.log, and run next dev again. It's gone the next time npm installs the package, which is what you want for an experiment. Use the .map file to find the line, as /where did.

Reading the source with a debugger

A stack trace shows one moment. A debugger lets you stop there and look at every variable. The server's code runs in Node.js, so you attach to Node's inspector, as Debugging showed for your own code. This is the tea shop under next dev --inspect:

next dev --inspect, then a request to /whereNext.js
app/where/
app/where/route.ts
import { readFileSync } from "node:fs";import { SourceMap } from "node:module";import { connection } from "next/server"; // One line of a stack trace: "at name (/…/node_modules/next/dist/file.js:row:column)".const FRAME = /at (?:async )?(?:(\S+) \()?(\S+\/node_modules\/next\/(dist\/\S+?\.js)):(\d+):(\d+)/; function fromSource(line: string) {  const frame = FRAME.exec(line);  if (!frame) return [];  const [, name = "(anonymous)", file, distFile, row, column] = frame;  const map = new SourceMap(JSON.parse(readFileSync(`${file}.map`, "utf8")));  const entry = map.findEntry(Number(row) - 1, Number(column) - 1);  if (!("originalSource" in entry)) return [];  const source = entry.originalSource.replace(/^.*?src\//, "packages/next/src/");  return [`${distFile}:${row}  ->  ${source}:${entry.originalLine + 1}  ${name}`];} export async function GET() {  Error.stackTraceLimit = 40;  const stack = new Error().stack ?? "";  await connection();  return new Response(stack.split("\n").flatMap(fromSource).join("\n"));}
Recorded from Next.js 16.4.3
$ next dev --inspectDebugger listening on ws://127.0.0.1:9229/<id>For help, see: https://nodejs.org/learn/getting-started/debugging▲ Next.js 16.4.3 (Turbopack)- Local:         http://127.0.0.1:3000- Network:       http://127.0.0.1:3000- Debugger port: 9229✓ Ready- Cache Components enabled- Partial Prefetching enabled

The Terminal tab starts with Debugger listening on ws://127.0.0.1:9229/<id> (the recorder used a free port and shows it as 9229). That line comes from the process that runs the server. next dev starts that process as a second Node.js process, and passes the flag on to it; From next dev to a response shows the two processes. This is the code that decides which inspector each one gets:

The server process's inspector, from cli/next-dev.ts at v16.4.3TypeScript
const nodeDebugType = getNodeDebugType(nodeOptions)
const originalAddress =
  nodeDebugType === undefined ? undefined : nodeOptions[nodeDebugType]
delete nodeOptions.inspect
delete nodeOptions['inspect-brk']
delete nodeOptions['inspect_brk']
if (nodeDebugType !== undefined) {
  const address = getParsedDebugAddress(originalAddress)
  address.port = address.port === 0 ? 0 : address.port + 1
  nodeOptions[nodeDebugType] = formatDebugAddress(address)
} else if (options.inspect) {
  const address: DebugAddress =
    options.inspect === true
      ? getParsedDebugAddress(true)
      : options.inspect
  nodeOptions.inspect = formatDebugAddress(address)
}

Line 1 looks for --inspect or --inspect-brk in the NODE_OPTIONS environment variable. Lines 4 to 6 remove them from what the server process gets. If they were there, lines 8 to 10 give the server the same option on the next port: the next dev process itself already took the first one, since NODE_OPTIONS reached it too. If you used the flag --inspect instead, lines 11 to 16 pass it on, and only the server listens. So:

  • next dev --inspect: one inspector, the server's, on 9229.
  • NODE_OPTIONS=--inspect next dev: next dev's own process on 9229, the server on 9230. Attach to 9230 to stop in route code. Next.js's debugging guide uses this form for --inspect-brk, which waits for a debugger before running any code.

This comes from reading lines 486 to 502 of next-dev.ts; the recording shows only the first case. Once attached (in Chrome, open chrome://inspect and click inspect), DevTools shows the server files in node_modules/next/dist, like router-server.js, as their TypeScript, because their .map files hold the source text. With the ignore list changed (below), a breakpoint on line 923 of router-server.ts stops for every request.

Two things stop people the first time:

  1. The right file. A Route Handler or a page doesn't run from dist/server/route-modules/app-route/module.js; that file is never loaded on Node.js. It runs from the bundle module.compiled.js picked, like app-route-turbo.runtime.dev.js. Put the breakpoint there, through its source map.
  2. The ignore list. DevTools skips Next.js's server code when you step, and hides its frames in the call stack. Their source maps ask it to:
Ignore-listing each compiled file's sources, from taskfile-swc.js at v16.4.3JavaScript
// ignore-list everything
sourceMapPayload.ignoreList = sourceMapPayload.sources.map(
  (source, sourceIndex) => {
    return sourceIndex
  }
)

For each file the SWC task compiles, lines 2 to 6 put every source of its map in the map's ignoreList, the list of sources a debugger should treat as library code. The runtime bundles in the /where stack have maps that list their sources there too. So, by default, stepping goes from your code to your code, which is what you want most days. To step into Next.js, change DevTools' ignore list setting (in its settings, under Ignore list), or show the ignore-listed frames in the call stack. The code is in lines 170 to 175 of taskfile-swc.js.

A question, and the folder to open

Most visits to the repository start with a question about your own app: why does this redirect win, why is this page ◐, why did the build stop. The first step is always the same: pick the folder. Sort these questions by the folder you'd open first.

Which folder holds the answer?
  • What next dev does with --inspect before the server starts
  • The list of commands that next accepts
  • Whether a redirect or a rewrite wins for the same address
  • How next start opens its HTTP server
  • How the build picks ○ or ◐ for each route
  • How Next.js finds the Rust compiler for your computer
  • What <Link> prefetches when it comes on screen
  • The code that hydrates the first HTML in the browser
  • The compile step that turns "use client" into a client reference
  • How Turbopack remembers what it already compiled
Try it yourself
0 of 10 correct

Sort each question by the part of the repository you'd open first.

Choose a category for every card. You can change an answer at any time; Reset clears them all.

Then search inside it. GitHub's search at the tag works, and so does searching node_modules/next/dist for an error message the app printed: most messages are written out in the source, so the file that throws it is one search away. When the code you find is internal, like a file name in dist or a manifest's shape, read it to understand your app, but never make your app depend on it: it can change in any release.

Common mix-ups

  • "node_modules/next is the source." No. It's dist: compiled JavaScript. The server files, like base-server.js, have a .map beside them that holds the TypeScript and leads back to it.
  • "Every file in src becomes one file in dist." Most do. The route runtimes don't: many files, React included, are bundled into each …runtime.prod.js and …runtime.dev.js.
  • "The App Router uses the React in my package.json." No. In this version it runs 19.3.0-canary-278794d7-20261002 from next/dist/compiled/react. The Pages Router uses yours.
  • "SWC lives in the Next.js repository." No. SWC is its own project. The repository has Next.js's transforms for it, in crates/next-custom-transforms, and Turbopack, in turbopack/crates.
  • "npm compiles the Rust when it installs Next.js." No. It downloads a .node file that was compiled for your platform before the release.
  • "The debugger can't see Next.js's code." It can. The source maps of Next.js's server files ask it to skip that code, through ignoreList. Change the setting, and you can step into it.

Each row is one of those beliefs, and what the recordings showed instead:

What you might expect, and what happened
What you might expectWhat happened
node_modules/next holds the TypeScript source.It holds dist: compiled JavaScript, .map files that carry the source text, and the bundled runtimes.
A breakpoint in dist/server/route-modules/app-route/module.js stops a Route Handler.That file isn't loaded on Node.js. The handler runs from dist/compiled/next-server/app-route-turbo.runtime.prod.js (or .dev.js).
Your package.json's React renders App Router pages.The App Router runs the canary React inside next/dist/compiled/react. Your react package serves the Pages Router.

Practice exercises

These exercises are about the repository and recorded projects, which can't run inside this page. Answer first, then use the hints.

Exercise 1 · Warm-upFind where next dev starts

You type next dev. Which file of packages/next/src runs first?

Answer, then press Check. Spacing and letter case don’t matter.

    Exercise 2 · Warm-upRead the Pages Router's React

    In the tea shop with both routers, which React version does pages/legacy.tsx run on?

    Answer, then press Check. Spacing and letter case don’t matter.

      Exercise 3 · PracticeFollow a frame back to src

      Which file and line of the repository does this line of the stack come from?

      Starter codeText
      dist/server/lib/router-server.js:567  ->  ?

      Answer, then press Check. Spacing and letter case don’t matter.

        Exercise 4 · PracticePredict the runtime file under next dev

        The tea shop runs under next dev with Turbopack. Which file does its Route Handler's runtime come from?

        Starter codeJavaScript
        if (process.env.NODE_ENV === 'development') {
          if (process.env.TURBOPACK) {
            module.exports = require('next/dist/compiled/next-server/app-route-turbo.runtime.dev.js')
          } else {
            module.exports = require('next/dist/compiled/next-server/app-route.runtime.dev.js')
          }
        } else {
          if (process.env.TURBOPACK) {
            module.exports = require('next/dist/compiled/next-server/app-route-turbo.runtime.prod.js')
          } else {
            module.exports = require('next/dist/compiled/next-server/app-route.runtime.prod.js')
          }
        }

        Answer, then press Check. Spacing and letter case don’t matter.

          Exercise 5 · ChallengeFix a breakpoint that never stops

          A teammate puts a breakpoint in node_modules/next/dist/server/route-modules/app-route/module.js, runs the tea shop with next start under the inspector, and asks for /where. It never stops. Which file should the breakpoint go in?

          Answer, then press Check. Spacing and letter case don’t matter.

            Exercise 6 · ChallengeFind Next.js's own SWC transforms

            The code that rewrites a file starting with "use client" while compiling is an SWC transform. Which crate holds it?

            Starter codeText
            crates/
              next-api
              next-build
              next-build-test
              next-code-frame
              next-core
              next-custom-transforms
              next-napi-bindings
              next-taskless
              wasm

            Answer, then press Check. Spacing and letter case don’t matter.

              Check your understanding

              For each question, ask which half of the repository holds the code, and which file in dist runs it.

              The Next.js source tour quiz · 7 questionsScore: first tries count
              1. Question 1 of 7In which folder of vercel/next.js is the code that runs when you type next start?

                Choose an answer to see the explanation.

              2. Question 2 of 7Under next start, the stack shows r0.handle; under next dev, AppRouteRouteModule.handle. Why?

                Choose an answer to see the explanation.

              3. Question 3 of 7Which React renders a Server Component in the App Router of Next.js 16.4.3?

                Choose an answer to see the explanation.

              4. Question 4 of 7This line came from the tea shop's stack. Which line of packages/next/src does it point to?

                Read the code, then predictText
                dist/server/base-server.js:1008  ->  ?  NextNodeServer.handleRequestImpl

                Choose an answer to see the explanation.

              5. Question 5 of 7What's inside the @next/swc-darwin-arm64 package that npm installed for the recording?

                Choose an answer to see the explanation.

              6. Question 6 of 7You run NODE_OPTIONS=--inspect next dev. Which inspector port does the server process get?

                Choose an answer to see the explanation.

              7. Question 7 of 7In DevTools, stepping into a Next.js function skips it, and its frames are hidden. Why?

                Choose an answer to see the explanation.

              Key takeaways

              • vercel/next.js holds the next package in packages/next/src: bin and cli for the commands, server for requests, build for next build, client for the browser, shared and lib for helpers.
              • SWC compiles most source files one by one into dist, with a .map beside each. The route runtimes are bundled into files like app-route-turbo.runtime.prod.js, and module.compiled.js picks one.
              • The Rust crates, in crates and turbopack/crates, are compiled into one .node file per platform, published as @next/swc-<platform>. SWC itself is a separate project.
              • The App Router runs the canary React in next/dist/compiled/react (19.3.0-canary-278794d7-20261002 in 16.4.3); the Pages Router runs the app's own react.
              • To build Next.js, follow its contributing guides: pnpm, pnpm build or pnpm dev, and pnpm swc-build-native only for Rust.
              • A debugger attaches to the server process. The source maps of Next.js's server files hold the TypeScript, and ignore-list it on purpose.

              Remember the one-liner.
              A line of Next.js's server code in node_modules/next/dist has a .map beside it that leads back to packages/next/src, at the tag of the version you run.

              Coming next: From next dev to a response. It follows one request through those files: the process next dev starts, the router server, the order of redirects, proxy.ts and rewrites, and the server classes that hand the request to a render.

              CompleteFrontend Clear concepts. Working examples.