Guide
Node.js fundamentals explained
Node.js is a JavaScript runtime that runs outside the browser — on servers, build tools, CLIs, and edge workers. It is not a web framework like Express or Fastify; those sit on top of Node. At its core, Node bundles Google’s V8 engine (which compiles JavaScript to machine code) with libuv (a C library that handles non-blocking I/O, timers, and a thread pool for file and crypto work). That combination made it practical to write network-heavy backends in the same language as front-end code. Today Node powers payment webhooks, real-time dashboards, SSR for React apps, and countless APIs — including settlement services that verify on-chain transactions. This guide covers what Node actually is, how modules and npm work, the async model that defines performance, and the habits that keep production Node services reliable.
What Node.js is (and is not)
When Ryan Dahl released Node in 2009, the novelty was not JavaScript itself but event-driven, non-blocking I/O on a single thread. Traditional server frameworks spawned a thread per connection; under load, thousands of blocked threads consumed memory. Node accepts many concurrent connections on one thread by delegating slow work (disk, network, DNS) to the OS and resuming when callbacks fire — the same event loop model browsers use, extended for server APIs.
Node is not multi-threaded JavaScript by default. CPU-bound work — image resizing, video encoding, heavy JSON parsing on megabyte payloads — still blocks the loop and stalls every other request. Frameworks like Express do not change that; you offload CPU to Worker threads, child processes, or external services. Node excels at I/O-bound workloads: REST APIs, GraphQL gateways, proxy layers, WebSocket fan-out, and glue between databases and queues.
Runtime vs framework vs platform
- Runtime: Node.js executes your
.jsfiles, provides built-in modules (fs,http,crypto), and manages the process lifecycle. - Framework: Express, Fastify, Hono, NestJS — routing, middleware, and HTTP ergonomics on top of Node’s
httpmodule. - Platform: Deno and Bun are alternative runtimes with different security defaults and tooling; they remain JavaScript/TypeScript ecosystems but are not drop-in identical to Node.
Modules: CommonJS, ESM, and package.json
Node organizes code into modules. For years the default was
CommonJS (require() / module.exports).
Modern Node and the wider ecosystem have shifted toward ECMAScript modules
(import / export). Both coexist; mixing them incorrectly
is a common source of “Cannot use import statement outside a module”
errors.
Choosing a module system
- Add
"type": "module"topackage.jsonto treat.jsfiles as ESM by default. - Use the
.mjsextension for ESM or.cjsfor CommonJS when you need both in one project. - Dynamic
import()works in CommonJS for lazy-loading ESM dependencies. - TypeScript projects typically compile to one or the other; align
tsconfigmoduleandmoduleResolutionwith your Node target.
package.json is the project manifest: name, version, entry point
("main" or "exports"), scripts ("start",
"test"), dependency lists, and engine constraints
("engines": { "node": ">=20" }). The "exports"
field (Node 12+) defines public import paths and enables encapsulation — consumers
cannot deep-import internal files you did not export.
npm, npx, and dependency hygiene
npm (Node Package Manager) installs libraries from the npm
registry into node_modules/ and records exact or ranged versions
in package-lock.json (or pnpm-lock.yaml / yarn.lock
if you use those tools). Lockfiles are essential: without them, two deploys
with the same package.json can resolve different transitive
versions and behave differently in production.
Scripts and local binaries
npm run <script> executes commands defined in
package.json with node_modules/.bin on the PATH.
npx runs a package binary without global install — useful for
one-off scaffolding. In CI and production, prefer npm ci (clean
install from lockfile) over npm install for reproducible builds.
Security and supply chain
- Audit with
npm audit; pin critical dependencies; review major upgrades. - Prefer well-maintained packages with clear licenses; avoid typosquatting names.
- Run installs as a non-root user inside containers; never commit
.envsecrets. - Use
enginesand.nvmrcso dev and prod run the same Node major version.
Core built-in modules you will use daily
Node ships a rich standard library. You rarely need third-party packages for basics:
fs/fs/promises— read and write files; prefer promises or streams over sync APIs in servers.path— join paths in an OS-safe way (path.join,path.resolve).http/https— create HTTP servers and clients; frameworks wrap these.crypto— hashing (SHA-256), HMAC verification, random bytes; pair with JWT libraries for auth tokens.events—EventEmitterfor pub/sub inside a process.child_process/worker_threads— spawn shells or isolate CPU work.process— environment variables (process.env), argv, exit codes, signals.
Environment variables are the standard way to inject config (database URLs, API
keys, feature flags) without hardcoding. Load .env files only in
development; production should inject env vars via your orchestrator
(Docker,
Kubernetes, systemd).
Async patterns: callbacks, Promises, and async/await
Node’s early APIs used callbacks with the error-first convention
(err, result) => {}. Nested callbacks produced “callback
hell.” Modern code uses Promises and
async/await, which compile to the same event-loop scheduling
but read synchronously.
import { readFile } from 'node:fs/promises';
export async function loadConfig(path) {
try {
const raw = await readFile(path, 'utf8');
return JSON.parse(raw);
} catch (err) {
console.error('config load failed', err);
throw err;
}
}
Critical rules for server code:
- Never use sync file APIs (
readFileSync) on the hot request path. - Always
awaitasync handlers in HTTP frameworks — unhandled rejections crash Node 15+ by default. - Parallelize independent I/O with
Promise.all(); sequence dependent steps with await. - Set timeouts on outbound HTTP and database calls so one slow dependency does not hold sockets open forever.
Streams, buffers, and backpressure
Large files and HTTP bodies should flow through streams rather
than loading entirely into memory. Node streams implement readable,
writable, duplex, and transform interfaces.
Piping (readable.pipe(writable)) wires data chunks with automatic
backpressure — if the consumer is slow, the producer pauses.
Buffers are fixed-length byte arrays for binary data (TLS,
images, protobuf). Strings and Buffers are not interchangeable; specify encodings
explicitly ('utf8', 'base64'). When proxying uploads,
stream from request to storage (S3, disk) instead of buffering multi-megabyte
bodies in RAM — a common outage pattern under traffic spikes.
Building HTTP services and webhooks
A minimal Node HTTP server listens on a port and handles requests in a callback
or framework route. Production services add structured logging, health checks
(/healthz), graceful shutdown on SIGTERM, and request ID propagation
for tracing. For inbound
webhooks (Stripe,
GitHub, chain indexers), verify signatures with crypto.createHmac,
respond quickly with 200, and enqueue heavy processing to a
message queue
so the provider does not retry endlessly while your handler is still parsing
payloads.
Place Node behind a reverse proxy (nginx, Caddy) or API gateway for TLS termination, rate limiting, and static file serving. The Node process should bind to localhost unless you have a specific reason to expose it directly.
Error handling, observability, and graceful shutdown
Uncaught exceptions and unhandled Promise rejections can terminate the process. Register handlers, but treat them as last resorts — fix the underlying bug. Use structured JSON logs (pino, winston) shipped to your log stack; correlate with metrics and traces via observability tooling.
On deploy, Kubernetes and systemd send SIGTERM. Stop accepting new connections, drain in-flight requests (with a timeout), close database pools, then exit. Without graceful shutdown, users see 502s mid-request during rolling updates.
When Node is the right choice (and when it is not)
Good fits: JSON APIs, BFF layers, SSR, WebSocket servers, CLI tooling in TypeScript, serverless functions with short I/O-heavy work, prototyping full-stack apps with shared types.
Poor fits: CPU-saturated computation (use Rust, Go, or Python with native libs), hard real-time guarantees, or workloads needing true parallel threads on shared mutable state without careful design.
Deno offers permission flags and built-in TypeScript; Bun targets speed with a bundled runtime and package manager. Many teams still standardize on Node LTS for ecosystem compatibility — especially libraries tied to native addons. Pick the runtime your team can operate, not the benchmark winner of the week.
Production checklist
- Run an LTS Node version; upgrade on a schedule, not reactively after CVEs.
- Commit lockfiles; use
npm ciin deploy pipelines. - Keep the event loop free — profile with
clinic.jsor async hooks if latency spikes. - Set
NODE_ENV=productionso frameworks enable caching and disable verbose errors to clients. - Limit request body size; validate input at the edge; never trust client-supplied amounts in payment flows.
- Health checks, metrics, structured logs, and graceful SIGTERM handling on every service.
Related reading
- JavaScript event loop explained — call stack, microtasks, and why blocking hurts servers
- REST API design explained — routes, status codes, and versioning on Node backends
- TypeScript fundamentals explained — typing Node APIs and async return types
- Docker fundamentals explained — packaging Node apps for reproducible deploys