Node.js & npm
Node.js is a program that runs JavaScript outside a web browser. It bundles Google’s V8 JavaScript engine with a C library called libuv that provides asynchronous file, network, and process I/O, plus a standard library of modules the browser never had — filesystem, sockets, child processes, crypto, streams.
That is the whole idea: take the language from the browser, drop the browser’s APIs, and add the APIs an operating-system-level program actually needs.
The two halves
Section titled “The two halves”| Piece | What it does |
|---|---|
| V8 | Parses, optimises, and executes JavaScript. Owns the heap, the garbage collector, and the JIT compiler. Written in C++. Also the engine in Chrome and Edge. |
| libuv | Provides the event loop, non-blocking sockets (epoll on Linux, kqueue on macOS, IOCP on Windows), a thread pool for work that has no async OS API (mostly filesystem), timers, and child process handling. Written in C. |
Node glues them together and exposes the libuv side to JavaScript as the node: core modules (node:fs, node:net, node:http, …). Nothing about Node changes the language; Array, Promise, class, and async/await behave exactly as they do in the browser, because it is the same engine.
What people build with it
Section titled “What people build with it”- HTTP servers and APIs — the original use case. Node handles many concurrent connections cheaply because each idle connection costs a file descriptor and a small object, not a thread.
- Command-line tools —
eslint,prettier,vite,tsc,npmitself. A CLI is just a JS file with a shebang; see tooling and publishing. - Frontend build tooling — bundlers, transpilers, dev servers, test runners. If you write for the web at all, you are running Node whether or not you write server code.
- Scripts and automation — glue that would otherwise be Bash or Python, written in a language you already know.
- Realtime services — WebSockets, chat, streaming, event fan-out, where the work is mostly waiting on I/O.
Node is a poor fit when the work is CPU-bound — video encoding, large matrix maths, cryptographic mining. One JavaScript thread does one thing at a time, so a long computation blocks everything. (Real options exist — worker_threads, native addons, or a separate service — but the default answer is “use another tool”.)
How Node differs from the browser
Section titled “How Node differs from the browser”Same language, completely different environment.
| Browser | Node | |
|---|---|---|
| Global object | window (globalThis) |
globalThis (no window) |
| DOM | document, Element, events |
none |
| Storage | localStorage, cookies, IndexedDB |
the filesystem, a real database |
| Filesystem | no access (except user-picked files) | node:fs, full access as the running user |
| Modules | ESM only, loaded over HTTP | ESM and CommonJS, loaded from disk |
| Networking | fetch, WebSocket, restricted by CORS |
raw TCP/UDP, HTTP servers, no CORS — CORS is a browser policy |
| Security model | sandboxed per origin | a normal OS process with your user’s permissions |
Globals that only exist in Node:
console.log(process.argv); // command-line argumentsconsole.log(process.env.HOME); // environment variablesconsole.log(process.platform); // 'linux' | 'darwin' | 'win32'console.log(__dirname); // CommonJS only — directory of the current fileconsole.log(Buffer.from('hi')); // <Buffer 68 69> — raw bytesGlobals Node adopted from the web platform, so the same code works in both places:
| Global | Available since |
|---|---|
URL, URLSearchParams, TextEncoder/TextDecoder |
Node 10 |
AbortController, AbortSignal |
Node 15 |
structuredClone |
Node 17 |
fetch, Request, Response, Headers, FormData |
Node 18 (on by default) |
WebSocket client |
Node 22 (on by default) |
The event loop
Section titled “The event loop”Node runs your JavaScript on one thread. It stays fast under load not by using more threads but by never blocking that one: every I/O call hands the work to the OS or a background thread and registers a callback.
The loop is a cycle of phases, run in order, over and over. Each phase has its own callback queue and drains it before moving on.
┌──────────────────────────┐┌─>│ timers │ setTimeout / setInterval callbacks whose time is up│ ├──────────────────────────┤│ │ pending callbacks │ a few deferred system callbacks (e.g. some TCP errors)│ ├──────────────────────────┤│ │ idle, prepare │ internal use only│ ├──────────────────────────┤ ┌───────────────┐│ │ poll │<────┤ incoming: │ retrieve new I/O events;│ ├──────────────────────────┤ │ connections, │ run their callbacks; block│ │ check │ │ data, etc. │ here if there is nothing else│ ├──────────────────────────┤ └───────────────┘│ │ close callbacks │ 'close' events, e.g. socket.on('close')│ └────────────┬─────────────┘└───────────────┘The poll phase is where the process spends its idle time. If there are no timers due and nothing scheduled, libuv blocks there waiting on the OS for socket readiness — which is why an idle Node server uses ~0% CPU.
Microtasks jump the queue
Section titled “Microtasks jump the queue”Between every callback — not just between phases — Node drains two extra queues, in this order:
- The
process.nextTickqueue. - The microtask queue (promise
.then/awaitcontinuations,queueMicrotask).
setTimeout(() => console.log('4 timeout'), 0);setImmediate(() => console.log('5 immediate'));Promise.resolve().then(() => console.log('3 promise'));process.nextTick(() => console.log('2 nextTick'));console.log('1 sync');
// 1 sync// 2 nextTick// 3 promise// 4 timeout <- timers phase// 5 immediate <- check phasesetImmediate runs in the check phase, right after poll. Inside an I/O callback it therefore always fires before a setTimeout(fn, 0); at the top level the order of those two is not guaranteed, because it depends on how long the process took to start.
The thread pool
Section titled “The thread pool”Not everything has a non-blocking OS API. libuv keeps a small thread pool (4 threads by default, set with the UV_THREADPOOL_SIZE environment variable) for:
- filesystem operations (
node:fs) dns.lookup(but notdns.resolve*, which uses the network directly)- some
node:cryptocalls —pbkdf2,scrypt,randomBytes node:zlibcompression
Network I/O does not use the pool; it uses the kernel’s event notification directly. So “Node is single-threaded” is true of your JavaScript and slightly false of the process as a whole.
Blocking is the one real sin
Section titled “Blocking is the one real sin”// Blocks the entire process — no other request is served while this runs.const data = fs.readFileSync('huge.json', 'utf8');
// Yields to the loop; other work continues.const data = await fs.promises.readFile('huge.json', 'utf8');Sync APIs are fine at startup (loading config, reading a certificate) and in short-lived CLI scripts. They are never fine inside a request handler. The same applies to tight CPU loops: a 200 ms JSON parse is 200 ms during which your server answers nobody.
Running Node
Section titled “Running Node”node app.js # run a filenode app.js --port 3000 # everything after the filename goes to process.argvnode --watch app.js # restart on file change (Node 18.11+, stable in 22+)node -e "console.log(1+1)" # evaluate a stringnode -p "process.version" # evaluate and print the resultnode # REPL — interactive promptThe REPL is a genuine scratchpad: it has top-level await, tab completion, _ for the last result, and .help for its dot-commands (.editor for multi-line input, .load file.js, .exit).
$ nodeWelcome to Node.js v22.11.0.> const { readFile } = require('node:fs/promises')> (await readFile('package.json', 'utf8')).length612> .exitUseful flags:
| Flag | Effect |
|---|---|
--watch |
Restart when an imported file changes. --watch-path=./src to widen the set. |
--env-file=.env |
Load environment variables from a file (Node 20.6+). No dotenv needed. |
--test |
Run the built-in test runner (Node 18+, stable from 20). |
--inspect / --inspect-brk |
Open the DevTools debugger port; attach from Chrome or VS Code. |
--max-old-space-size=4096 |
Raise V8’s heap limit, in MB. |
NODE_OPTIONS="..." |
Environment variable holding flags, for when you cannot change the command line. |
Versions, LTS, and nvm
Section titled “Versions, LTS, and nvm”Node releases a new major every six months. Even-numbered majors (18, 20, 22, 24) enter LTS — Long Term Support — the October after release and are then maintained for about 30 months total. Odd-numbered majors (21, 23, 25) are “Current”: useful for trying new features, never for production.
| Line | Meaning |
|---|---|
| Current | Newest features, breaking changes allowed, supported ~6 months. |
| Active LTS | Backported bug fixes and non-breaking features. The default choice. |
| Maintenance LTS | Critical and security fixes only, until end of life. |
Use the Active LTS unless you have a specific reason not to. Everything in this section targets Node 22+ and calls out anything newer.
nvm (Node Version Manager) lets you keep several versions side by side and switch per project. On macOS and Linux:
# install nvm (check github.com/nvm-sh/nvm for the current version string)curl -o- https://raw.githubusercontent.com/nvm-sh/nvm/v0.40.1/install.sh | bash# restart your shell, then:nvm install --lts # install the current LTSnvm install 20 # install a specific majornvm use 20 # switch this shell to itnvm alias default lts/* # what new shells getnvm ls # what you havePut the version in a .nvmrc at the repo root and nvm use picks it up with no argument:
22Also pin the version in package.json so other tools and CI can see it:
{ "engines": { "node": ">=22" }}engines is advisory for npm by default; add engine-strict=true to .npmrc to make a mismatch an install error.
A first program
Section titled “A first program”import { createServer } from 'node:http';
const server = createServer((req, res) => { res.writeHead(200, { 'content-type': 'application/json' }); res.end(JSON.stringify({ url: req.url, node: process.version }));});
server.listen(3000, () => console.log('http://localhost:3000'));{ "type": "module" }node --watch server.jscurl localhost:3000/hello# {"url":"/hello","node":"v22.11.0"}The "type": "module" line is what allows import here — the next page explains why.
Where to go next
Section titled “Where to go next”- Modules: CommonJS and ESM — the single most confusing part of Node.
- Core APIs —
process,fs,path, streams,EventEmitter,http. - npm and package.json — dependencies, semver, lockfiles, scripts.
- npm vs pnpm and workspaces — why
node_modulesis the way it is. - Tooling and publishing — TypeScript, testing, CLIs, security, shipping to the registry.
Key points
Section titled “Key points”- Node = V8 (the language) + libuv (async I/O) + a core library the browser lacks.
- Your JavaScript runs on one thread; concurrency comes from never blocking it.
- The event loop cycles through phases;
nextTickand promise callbacks run between every callback, not once per phase. - Filesystem, DNS lookup, some crypto and zlib use a 4-thread pool. Network I/O does not.
- Prefer async APIs everywhere except startup and one-shot scripts.
- Run the Active LTS, manage versions with
nvm, and record the version in.nvmrcandengines.