nodejs/node
Architecture
Node.js is a host program that embeds the V8 JavaScript engine, drives an event loop using libuv, and exposes platform capabilities (file system, networking, crypto, child processes, …) to JavaScript through C++ bindings. This page explains the layered architecture and how a process moves from int main() to "your index.js is running".
Layered architecture
graph TD
subgraph Userland
UJS["User JS (CommonJS / ESM / TypeScript via amaro)"]
end
subgraph "Public JS API (lib/*.js)"
Public["fs / http / crypto / net / stream / worker_threads / test / sqlite ..."]
end
subgraph "Internal JS (lib/internal/**)"
Internal["bootstrap, modules, streams, errors, http2, crypto, test_runner, ..."]
Bindings["internalBinding('xxx') -> C++"]
end
subgraph "C++ host (src/**)"
Env["Environment / Realm"]
Wraps["AsyncWrap / HandleWrap / BaseObject"]
Modules["node_*.cc binding modules (fs, crypto, http2, sqlite, ...)"]
end
subgraph "Native dependencies (deps/**)"
V8[V8 engine]
UV[libuv event loop]
OpenSSL[OpenSSL / ncrypto]
LLHTTP[llhttp]
NGHTTP2[nghttp2]
NGTCP2[ngtcp2 + nghttp3]
ADA[ada URL parser]
ICU[ICU]
SIMDJSON[simdjson]
SQLITE[SQLite]
UVWASI[uvwasi]
UNDICI[undici fetch/HTTP]
AMARO[amaro TS stripper]
end
UJS --> Public
Public --> Internal
Internal --> Bindings
Bindings --> Modules
Modules --> Env
Modules --> Wraps
Env --> V8
Wraps --> UV
Modules --> OpenSSL
Modules --> LLHTTP
Modules --> NGHTTP2
Modules --> NGTCP2
Public --> ADA
Modules --> ICU
Modules --> SIMDJSON
Modules --> SQLITE
Modules --> UVWASI
Public --> UNDICI
Internal --> AMAROThe layering is intentional: user code can only reach C++ through the public JS API. The public modules in lib/*.js re-export from lib/internal/**, which call into the runtime via internalBinding(name). Only modules listed in lib/internal/bootstrap/realm.js (the processBindingAllowList and the legacy wrapper list) are exposed through process.binding() at all; the internalBinding namespace is hidden from user code.
Process lifecycle
sequenceDiagram
autonumber
participant OS
participant main as src/node_main.cc
participant node_cc as node::Start (src/node.cc)
participant Init as InitializeOncePerProcess
participant Inst as NodeMainInstance
participant Realm as Realm::BootstrapRealm
participant Bootstrap as lib/internal/bootstrap/{realm,node}.js
participant Pre as lib/internal/process/pre_execution.js
participant Main as lib/internal/main/<entry>.js
participant User as user code
OS->>main: argv, envp
main->>node_cc: node::Start(argc, argv)
node_cc->>Init: parse CLI, init V8, init OpenSSL, init ICU
Init->>Inst: build NodeMainInstance with snapshot
Inst->>Realm: enter principal Realm
Realm->>Bootstrap: deserialize snapshot OR run realm.js + node.js
Bootstrap->>Pre: setupPreExecution(...)
Pre->>Main: select run_main_module / repl / eval / worker_thread / test_runner
Main->>User: require/import the user entry, run event loop
User-->>OS: exit codeSteps 4–5 are the embedded V8 startup snapshot. Node ships a snapshot generated at build time by tools/snapshot/ and SnapshotBuilder::Generate() in src/node_snapshotable.cc, so the bootstrap scripts do not actually re-run on every start — their resulting V8 heap is deserialized. Pass --no-node-snapshot to take the slow path.
The selectable main scripts live in lib/internal/main/:
| Main script | Selected when |
|---|---|
lib/internal/main/run_main_module.js |
Default — node app.js |
lib/internal/main/eval_string.js |
node -e "..." |
lib/internal/main/eval_stdin.js |
node reading from stdin |
lib/internal/main/repl.js |
Interactive REPL |
lib/internal/main/check_syntax.js |
node --check |
lib/internal/main/inspect.js |
node inspect … (inspector CLI) |
lib/internal/main/print_help.js |
node --help |
lib/internal/main/test_runner.js |
node --test |
lib/internal/main/watch_mode.js |
node --watch |
lib/internal/main/worker_thread.js |
Inside a Worker thread |
lib/internal/main/embedding.js |
Embedder-driven entry |
lib/internal/main/mksnapshot.js |
Building the startup snapshot |
lib/internal/main/prof_process.js |
--prof-process |
The full selection logic is in src/node.cc (search for LoadEnvironment) and lib/internal/process/pre_execution.js.
Realm and Environment model
Node has two C++ types that organize state:
node::Environment(src/env.h,src/env.cc) — process-/thread-wide state for one Node "instance". A worker thread has its ownEnvironment. Shared infrastructure lives here: the libuv loop, the inspector agent, the per-isolate options, performance state, exit handlers.node::Realm(src/node_realm.h,src/node_realm.cc) — the JS-side execution context. The "principal realm" is created when anEnvironmentboots; additionalShadowRealms and Realm-per-Worker contexts use this same primitive. Realms own bindings, the per-realm builtin loader, and the JS process object.
graph TD
Iso[V8 Isolate] --> Env[node::Environment]
Env --> PR[Principal Realm]
Env --> SR[ShadowRealm 1..N]
Env --> WT[Worker thread Env]
PR --> Bindings1["internalBinding(...) -> Realm-scoped storage"]
PR --> JsProcess[process object]
PR --> EventLoop[libuv loop]
SR --> Bindings2[Per-Realm bindings]See primitives › environment-and-realm for details, including the difference between an IsolateData, an Environment, and a Realm.
Async I/O: libuv handles, AsyncWrap, and the JS callback shape
Almost every I/O object in Node is a thin JS class wrapping a *Wrap C++ object that wraps a libuv handle or request:
graph LR
JSCls["JS class (e.g. net.Socket)"] --> Wrap["C++ TCPWrap : ConnectionWrap : StreamBase : AsyncWrap"]
Wrap --> UV["uv_tcp_t"]
UV --> Loop[libuv event loop]
Loop --> CB["uv callback -> Wrap::OnEvent"]
Wrap --> JSCB["MakeCallback() -> JS"]
JSCB --> JSClsBaseObject(src/base_object.h) is the common ancestor for any C++ object that has a JS wrapper; it knows how to be persisted across snapshots and how to be torn down at Realm cleanup.AsyncWrap(src/async_wrap.h) attaches an async-resource identity used byasync_hooks,AsyncLocalStorage, and diagnostic channels. It owns theMakeCallbackmachinery that runs JS callbacks while accounting forprocess.nextTick, microtasks, and the async stack.HandleWrap(src/handle_wrap.h) andReqWrap(src/req_wrap.h) are the two libuv flavors: long-lived handles vs. one-shot requests.StreamBase(src/stream_base.h) is the C++ side of Node's stream contract used by sockets, pipes, TLS, and HTTP/2.
A typical socket.write(buf, cb) call goes JS → StreamBase::Writev → uv_write → libuv → OnAfterWrite → MakeCallback → JS callback.
See primitives › async-wrap-and-handle-wrap and systems › streams.
Module system: CommonJS, ESM, and the loader split
Node implements two parallel module loaders that share a resolver and translators:
- CommonJS in
lib/internal/modules/cjs/(notablyloader.jsexporting theModuleclass). - ECMAScript Modules in
lib/internal/modules/esm/, structured asloader.js(the in-thread loader),hooks.js(the customization-hooks worker bridge),resolve.js,load.js,translators.js,module_job.js. - The ESM loader can hand resolution off to a worker (
lib/internal/modules/esm/worker.js) so user-supplied hooks run in isolation. - TypeScript stripping is implemented by integrating the
amaroRust crate, exposed vialib/internal/modules/typescript.js.
Permission Model
A capability-style sandbox lives behind --permission. Each domain (fs, child_process, worker, wasi, inspector, addon, net, ffi) has its own *_permission.{cc,h} under src/permission/, registered in src/permission/permission.cc. JS hooks live in lib/internal/process/permission.js. See systems › permission-model.
Diagnostics surface
Node exposes a layered diagnostics stack:
inspector(Chrome DevTools protocol) —src/inspector/,src/inspector_*andlib/inspector.js.async_hooksandAsyncLocalStorage—lib/async_hooks.js,lib/internal/async_hooks.js,lib/internal/async_local_storage/, backed byAsyncWrap.diagnostics_channel—lib/diagnostics_channel.jsandsrc/node_diagnostics_channel.{cc,h}.perf_hooks—lib/perf_hooks.js,lib/internal/perf/,src/node_perf.{cc,h}.trace_events—lib/trace_events.js,src/tracing/(perfetto-based when enabled).report—src/node_report.cc(process.report).
Build pipeline at a glance
graph LR
cfg[configure.py] --> gypi[config.gypi]
gypi --> gyp[node.gyp / common.gypi]
gyp --> ninja[Ninja or Make]
deps[deps/v8, libuv, openssl, ...] --> ninja
src[src/**] --> ninja
js2c[tools/js2c.cc] --> embedded[node_javascript.cc]
embedded --> ninja
ninja --> binary[node binary]
binary --> mksnap[node_mksnapshot]
mksnap --> snap[snapshot blob]
snap --> binaryThe tools/js2c.cc step compiles every .js and .mjs under lib/ and selected deps/ directories into a C++ source file that ships inside the node binary. That is why process.moduleLoadList reports module loads without ever touching the disk. See Vendored deps and build and systems › bootstrap-and-startup.
Built by Factory AutoWiki from public repository content. It is a generated preview for codebase exploration, not source-maintained documentation.