Chapter 1 · Getting Started

Introduction to Node.js

Node.js lets you run JavaScript outside the browser: on your laptop, on a server, in a build tool or a command-line app. This chapter explains what Node actually is (and isn't), how it can serve thousands of users with a single thread, how it differs from the browser, and how its versions work.

You will learn

  • What a JavaScript runtime is: V8 + libuv + the core modules.
  • Why Node uses a single thread with non-blocking I/O, and why that scales.
  • How Node differs from the browser: globals, APIs, modules and who controls the version.
  • What V8 does with your code, and how much JavaScript you need to know.
  • Node's release lines (Current vs LTS), version managers, and checking versions in code.

What is Node.js?

Node.js is a JavaScript runtime: a program that runs JavaScript files and gives them extra powers the language itself doesn't have, such as reading files, opening network connections and starting other programs. It's open source, cross-platform (Linux, macOS, Windows) and was created by Ryan Dahl in 2009.

Node is made of three main layers:

Your JavaScript (app.js, npm packages) Core modules: node:fs · node:http · node:events · node:stream · … V8runs JavaScript, manages memory libuvevent loop, async I/O, thread pool Operating system: files, network, processes, clock
Node = V8 (the engine) + libuv (the I/O machinery) + core modules (the API you call).

🧠 Analogy: engine and car. V8 is an engine. On its own it can't take you anywhere: it has no wheels, doors or dashboard. Chrome puts that engine in one car (with a DOM, tabs and a window), and Node puts the same engine in a different car (with a file system, network sockets and processes). Same engine, same JavaScript, very different vehicles.

Your first Node program

Save this as hello.js and run it with node hello.js:

There's no HTML page and no browser: console.log writes straight to your terminal (to standard output).

The classic example: a web server

The first example on nodejs.org is a web server in a few lines. Here's a version that starts a server, sends itself one request with fetch, prints the answer and shuts down, so it runs to completion:

In a real app you'd use a fixed port, like server.listen(3000), and the server would keep running until you stop it with Ctrl+C. Chapter 11 builds servers properly.

One thread, non-blocking I/O

A Node program runs your JavaScript on one main thread. Yet a single Node process can serve thousands of connections at once. How?

Most of a server's time is spent waiting: for the disk, a database or the network. In Node, I/O operations are non-blocking: you start them, give a callback (or get a promise), and Node keeps running other code. When the operating system says "the data is ready", Node queues your callback, and the event loop runs it as soon as the thread is free.

🧠 Analogy: one waiter, many tables. A restaurant with one waiter could work in two ways. A blocking waiter takes an order, walks to the kitchen and stands there until the dish is ready, ignoring every other table. A non-blocking waiter takes the order, pins it in the kitchen, and goes on to the next table; when the bell rings, the waiter carries the finished dish out. Node is the second waiter: the kitchen (the operating system and libuv) does the slow work, and the single waiter (your JavaScript thread) never stands still.

☕ Java: a classic Java servlet server uses one thread per request; a request waiting on the database blocks its thread. Node instead uses one thread plus asynchronous I/O, like Java's NIO/Netty or virtual threads in spirit. You don't share memory between request handlers on different threads, so there are no data races or locks, but a slow CPU computation blocks everyone (Chapter 13).

⚠️ Pitfall: non-blocking is about I/O, not CPU. A loop that computes for two seconds blocks the only thread: no other request, timer or callback runs meanwhile. Node is great for I/O-heavy work (APIs, proxies, real-time apps, tools) and needs extra care (worker threads) for heavy computation.

Node vs the browser

Both run JavaScript with V8-class engines and both use an event loop, so the language is identical. The environment is different:

BrowserNode.js
Global objectwindow (and globalThis)globalThis (no window)
Page APIsdocument, DOM, localStorage, cookiesnone of these
System APIssandboxed: no file systemnode:fs, node:net, node:child_process, process
Shared web APIsfetch, URL, setTimeout, AbortController, TextEncoder, structuredClone, WebSocket, crypto.randomUUID…
ModulesES modules (<script type="module">)ES modules and CommonJS (require)
JavaScript versionwhatever your users' browsers supportyou choose the Node version, so you know exactly
Security modelthe page is untrusted, heavily sandboxedyour program has your user's full permissions

🌐 Browser habit to unlearn: in the browser you never control your users' browser versions, so you transpile and polyfill. In Node, the version is a deployment decision. If your server runs Node 26, you can use every feature Node 26 supports — no Babel needed.

How much JavaScript do you need?

Node is JavaScript, so the more you know the language, the easier Node becomes. Before going deep, be comfortable with these topics (the links go to the JavaScript Study Book):

The V8 engine

V8 is written in C++ and turns JavaScript into machine code while the program runs (just-in-time compilation). It starts by interpreting your code quickly (the Ignition interpreter), watches which functions run often ("hot" code) and recompiles those with an optimising compiler (TurboFan, with mid-tier compilers in between). It also manages memory with a garbage collector.

🧠 Analogy: an interpreter who becomes a translator. On day one, a conference interpreter translates each sentence as it comes. After hearing the same phrases a hundred times, they write a polished translation of those phrases and just read it out. That's V8: it interprets first, then optimises the code you run most. It's also why micro-benchmarks in JavaScript are tricky: the first runs are slower than later ones.

You can see which engine and library versions your Node uses:

node -p ("print") evaluates an expression and prints the result; you'll see it often in this book. Other engines exist (SpiderMonkey in Firefox, JavaScriptCore in Safari and Bun), but Node always uses V8.

Node versions and release lines

A new major version of Node is released every six months (in April and October). The release schedule works like this:

So Node 24 (codename Krypton) and Node 26 (codename Lithium) are both LTS lines in late 2026. Your code can find out what it's running on:

Version managers

Different projects may need different Node versions. A version manager installs several versions side by side and switches between them per project:

Most version managers read a .nvmrc or .node-version file in the project folder, containing just a version like 26, so everyone on the team uses the same one.

💡 Tip: avoid installing Node with sudo or system package managers that lag behind. A version manager installs Node in your home folder, so global npm packages don't need administrator rights.

Modern JavaScript in Node

Node follows V8, so new language features arrive in Node a few months after they ship in Chrome. Everything from ES2015 (ES6) on is available in every supported Node version: classes, arrow functions, promises, async/await, optional chaining, private fields, top-level await in modules, and newer features like iterator helpers and Set methods. To check a specific feature, look at node.green or simply try it:

More examples

Facts about the machine

The node:os module describes the computer your program runs on. Your values will differ, so this example only prints the shape of the data:

One script, two environments

Code that should run both in the browser and in Node can check for the features it needs instead of guessing the environment:

Showing that the thread is single

Two timers are due after 10 ms, but a busy loop holds the thread for 50 ms first. The timers can only run once the thread is free:

Recap

  • Node.js is a JavaScript runtime: V8 runs the code, libuv does the I/O and runs the event loop, and core modules (node:fs, node:http…) give you system powers.
  • Your JavaScript runs on one thread. I/O is non-blocking, so one process can juggle thousands of connections; CPU-heavy work blocks everyone.
  • Same language as the browser, different environment: no window/document, but process, files, sockets and both module systems. You choose the Node version.
  • V8 compiles hot code just in time. Even-numbered majors become LTS; use LTS in production and a version manager (nvm, fnm, Volta) on your machine.

Exercises

Exercise 1 🟢 easy run it

Write greet.js that prints Hello, Node! and the current Node major version on a second line, like running on Node 26.

Show solution

Exercise 2 🟢 easy predict the output

What does each line print when run with node?

Show solution

undefined, object, true, string. Node has no window or document; globalThis is the standard name for the global object everywhere; process.version is a string like "v26.13.0".

Exercise 3 🟢 easy explain

Name the three main layers of Node.js and what each one does, in one sentence each.

Show solution

V8 parses, compiles and runs JavaScript and manages memory. libuv provides the event loop, asynchronous I/O (files, network, timers) and a thread pool. The core modules (node:fs, node:http, …) are the JavaScript API that connects your code to libuv and the operating system.

Exercise 4 🟢 easy choose

Your company is starting a new production API today. Should it target the newest Current release or an LTS release? And how would you make sure every developer uses the same version?

Show solution

An LTS release: it gets bug and security fixes for about 30 months, while Current lines change quickly and odd-numbered ones never become LTS. Pin the version in a .nvmrc (or .node-version) file read by version managers, and document it in package.json's "engines" field.

Exercise 5 🟡 medium predict the output

In what order are the lines printed, and why?

Show solution

The timer is due almost immediately, but its callback can only run when the main thread is free. The synchronous code (B, the 20 ms loop, C) runs to completion first; then the event loop runs the timer.

Exercise 6 🟡 medium build it · playground

Create playground/check-node.js: exit with code 1 and an error message on stderr if the Node major version is lower than a minimum given in a constant MIN = 22; otherwise print OK: Node 26 ≥ 22 (with the real numbers).

Show solution

Errors go to console.error (stderr) and a non-zero exit code tells scripts and CI that something failed (Chapter 2).

Exercise 7 🟡 medium explain why

A colleague says: "Node is single-threaded, so it can only handle one HTTP request at a time." Explain what's right and what's wrong with this statement, using the waiter analogy.

Show solution

Right: your JavaScript runs on one thread, so only one callback executes at any instant. Wrong: requests are not handled "one at a time" from start to finish. While one request waits for the database or the disk, Node starts on others, like a waiter who pins an order in the kitchen and serves the next table. Thousands of requests can be in progress at once; only the short bursts of JavaScript between waits take turns. (Behind the scenes, libuv also uses OS facilities and a thread pool to do the actual I/O.)

Exercise 8 🟡 medium build it

Write runtime.js with a function describeRuntime() that returns "browser" if window and document exist, "node" if process.versions.node exists, and "unknown" otherwise. Print the result.

Show solution

Use typeof for the checks: reading an undeclared global like window directly would throw a ReferenceError in Node.

Exercise 9 🔴 hard measure it

Show the cost of blocking: start a setInterval that should tick every 10 ms, block the thread for 100 ms after 25 ms, and print how many ticks happened in the first 200 ms. Explain why it's fewer than 20.

Show solution

An interval doesn't "catch up" on missed ticks. During the 100 ms block no callbacks can run, so roughly ten ticks are simply lost. In a server, the same block would delay every request in flight by 100 ms.

Exercise 10 🔴 hard mini-project · playground

Create playground/hello-server.js: an HTTP server that answers / with Hello!, /time with JSON { "uptimeSeconds": … } (use process.uptime()), and anything else with status 404. Make the script test itself with fetch on a free port and print the three status codes, then close.

Show solution

writeHead returns the response object, so you can chain .end(). You'll build a proper router with JSON bodies in Chapter 11.

Official docs