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. readFile β†’ JSON.parse β†’ push the new task β†’ JSON.stringify β†’ writeFile, 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 β†’