Original notes for JavaScript360.org — topic order inspired by Eloquent JavaScript (4th ed.) by Marijn Haverbeke. See the full attribution note.
Chapter 20: Node.js
Estimated time: ~40 minutes · Chapter type: Concept
What you'll learn
- What Node.js actually is, and how it differs from browser JavaScript
- Reading and writing files
npmandpackage.json- Building a minimal HTTP server from scratch
What Node.js is
Node.js takes the same JavaScript engine that powers Google Chrome (V8) and runs it outside the browser, as a standalone program on your operating system. That single move is what lets JavaScript run command-line tools, backend servers, build scripts, and more — anywhere a general-purpose runtime is useful, not just inside a webpage.
Because there's no browser involved, Node.js has no document, no window, and no DOM — none of Chapters 13–17 apply here. In exchange, it gives you things the browser sandbox (Chapter 13) deliberately withholds: file system access, the ability to open network sockets and listen for connections, and access to environment variables and process information.
console.log(process.argv); // command-line arguments the script was invoked with
console.log(process.env.HOME); // an environment variable
The module system, revisited
Chapter 10 covered ES modules and CommonJS in general; Node.js is where you'll actually use both day to day. A modern Node project typically sets "type": "module" in package.json and uses import/export throughout, but you'll still encounter CommonJS (require/module.exports) constantly in existing packages and older code.
Reading and writing files
The built-in fs (file system) module is one of the most-used pieces of Node's standard library. Prefer its Promise-based API (fs/promises) so you can use async/await naturally, as covered in Chapter 11:
import { readFile, writeFile } from 'node:fs/promises';
async function loadConfig(path) {
const text = await readFile(path, 'utf8');
return JSON.parse(text);
}
async function saveConfig(path, config) {
await writeFile(path, JSON.stringify(config, null, 2));
}
path (another built-in module) helps you build file paths correctly across operating systems, rather than hand-concatenating strings with / or \:
import path from 'node:path';
path.join('data', 'users', 'profile.json'); // "data/users/profile.json" (or the OS-correct separator)
path.extname('photo.png'); // ".png"
Streams, briefly
Reading an entire large file into memory at once (as readFile does) is fine for small files but wasteful — or outright impossible — for huge ones. Streams let you process data in small chunks as it arrives, rather than waiting for the whole thing:
import { createReadStream } from 'node:fs';
const stream = createReadStream('huge-log-file.txt', { encoding: 'utf8' });
stream.on('data', (chunk) => {
// process one chunk at a time — memory usage stays flat regardless of file size
});
stream.on('end', () => console.log('Done reading'));
You won't need streams for every task, but it's worth recognizing the pattern — anywhere you see .on('data', ...), something is being processed incrementally rather than all at once.
npm and package.json
Every Node project has a package.json describing it: its name, version, dependencies, and the scripts you can run against it.
{
"name": "my-app",
"version": "1.0.0",
"type": "module",
"scripts": {
"start": "node index.js",
"test": "node --test"
},
"dependencies": {
"some-package": "^2.1.0"
}
}
npm install some-package adds it to dependencies and downloads it into node_modules; npm run start (or the shorthand npm start for the conventional start script) executes the corresponding script. This is the same package ecosystem introduced in Chapter 10 — Node.js is simply where most of it actually gets installed and run.
Building a minimal HTTP server
Node's built-in http module is low-level compared to a framework like Express, but understanding it directly demystifies what every web framework is doing underneath. A server is, at its core, a function that receives each incoming request and decides how to respond:
import { createServer } from 'node:http';
const tasks = [{ id: 1, title: 'Learn Node.js' }];
const server = createServer((request, response) => {
if (request.method === 'GET' && request.url === '/api/tasks') {
response.writeHead(200, { 'Content-Type': 'application/json' });
response.end(JSON.stringify(tasks));
return;
}
if (request.method === 'POST' && request.url === '/api/tasks') {
let body = '';
request.on('data', (chunk) => { body += chunk; }); // the request body arrives as a stream
request.on('end', () => {
const newTask = JSON.parse(body);
newTask.id = tasks.length + 1;
tasks.push(newTask);
response.writeHead(201, { 'Content-Type': 'application/json' });
response.end(JSON.stringify(newTask));
});
return;
}
response.writeHead(404, { 'Content-Type': 'text/plain' });
response.end('Not found');
});
server.listen(3000, () => console.log('Listening on http://localhost:3000'));
Notice the request body arriving via the same stream.on('data'/'end') pattern from the section above — Node's low-level APIs are consistent this way once the pattern clicks. This is also, quite literally, a GET/POST HTTP server as described in Chapter 18, just implemented instead of consumed.
Key takeaways
- Node.js runs JavaScript outside the browser using the same engine (V8), trading DOM/browser APIs for file system, networking, and OS-level access.
- Prefer the Promise-based
fs/promisesAPI withasync/awaitover older callback-based file APIs. package.jsonandnpmmanage a project's metadata, scripts, and third-party dependencies.- The built-in
httpmodule shows you what's actually happening underneath any web framework: a function that inspects each request and writes a response.
Try it yourself
- Write an async function that reads a JSON file of tasks, adds a new task, and writes the updated list back to the same file.
- Extend the minimal HTTP server above to support
DELETE /api/tasks/:id(hint: you'll need to parse the id out ofrequest.urlyourself, since there's no router here). - Explain, in your own words, why streaming a large file in chunks uses less memory than
readFile-ing it all at once.
Hints
readFile→JSON.parse→ push the new task →JSON.stringify→writeFile, allawaited in sequence since each step depends on the previous one.request.url.match(/^\/api\/tasks\/(\d+)$/)to extract the id, thentasks.filter(t => t.id !== Number(id)).readFilehas to hold the entire file's contents in memory simultaneously; a stream only ever holds one small chunk at a time, regardless of the total file size.
Summary
Node.js is what makes "I know JavaScript" mean "I can build a backend, a CLI tool, or a build script," not just "I can add interactivity to a webpage." Our final project puts a Node server and browser-side JavaScript to work together — a genuinely full-stack build, and the natural capstone for everything this course has covered.