Skip to main content
Adapted, not copied

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
  • npm and package.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/promises API with async/await over older callback-based file APIs.
  • package.json and npm manage a project's metadata, scripts, and third-party dependencies.
  • The built-in http module shows you what's actually happening underneath any web framework: a function that inspects each request and writes a response.

Try it yourself

  1. 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.
  2. Extend the minimal HTTP server above to support DELETE /api/tasks/:id (hint: you'll need to parse the id out of request.url yourself, since there's no router here).
  3. Explain, in your own words, why streaming a large file in chunks uses less memory than readFile-ing it all at once.
Hints
  1. readFileJSON.parse → push the new task → JSON.stringifywriteFile, all awaited in sequence since each step depends on the previous one.
  2. request.url.match(/^\/api\/tasks\/(\d+)$/) to extract the id, then tasks.filter(t => t.id !== Number(id)).
  3. readFile has 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.

 

🌟 Join the JavaScript360 community

Connect with fellow JS developers, and get free interview prep, React challenges, and DSA guides.

Join on WhatsApp →