Without notes, state yesterday’s main idea and one unresolved question.
Node.js runtime and modules
HTTP, Node.js, and APIs
Objective
Run JavaScript outside the browser and access server-side capabilities.
HTTP is a request/response protocol like a defined communication bus: method, address, headers, body, and status all have roles.
- Node runtime
- ES modules
- filesystem and environment differences
Why this matters
Yesterday you read HTTP exchanges that other people's servers produced. To produce your own you
need a program that runs on a machine you control, outside any browser. That program is
Node.js — which you installed on Day 2 and have been quietly using ever since to run
node file.js.
Today you find out what Node actually gives you that a browser never can: files, environment variables, exit codes, and eventually network sockets. By the end you will have a command-line tool that reads a JSON file of equipment records, refuses to trust it, and prints a summary — with clear errors when the file is missing or wrong.
What Node.js actually is
Node.js is a program that runs JavaScript. Inside it sits V8, the same JavaScript engine Chrome uses, wrapped in a library of things a server needs.
The language is identical. const, arrays, map, objects, async/await — everything from Weeks
3 and 4 works unchanged. What differs is the environment: the set of objects handed to your code
for free.
| In a browser you get | In Node you get |
|---|---|
document, window, the DOM |
process, Buffer, globalThis |
localStorage, alert |
node:fs (files), node:path, node:os |
| Fetch limited by page security rules | node:http (be a server, not just a client) |
Neither is a superset of the other. There is no document in Node — nothing is on screen, so there
is no page to manipulate. There is no node:fs in the browser — pages must not be able to read your
disk. Both have console.log, JSON, fetch, and timers, which is why so much code moves between
them.
Same core, different peripherals
V8 is the processor core; the browser and Node are two different boards built around it. The
browser board wires the core to a display controller and input buttons, and deliberately leaves
the storage bus unconnected — untrusted code is running on it. The Node board wires the same core
to storage, the filesystem, and a network interface, and leaves the display off entirely.
Identical instruction set, completely different pinout. Code that assumes the wrong board fails
immediately with document is not defined.
ES modules: splitting code across files
On Day 23 you learned modules — export what a file offers, import what it needs. Node supports
two module systems, and picking one is the first decision in any Node project.
CommonJS is the older system: require() and module.exports. You will meet it in existing
code. ES modules (ESM) is the standard one — import and export, the same syntax the browser
uses. Use ESM for everything you write.
Node decides which system a file uses by two signals: a file ending in .mjs is always ESM, and
any .js file in a project whose package.json contains "type": "module" is ESM. Without either,
Node treats .js as CommonJS and an import line fails.
Built-in modules are imported with a node: prefix, which makes it unmistakable that you mean
Node's own module and not a package from npm:
import { readFile } from "node:fs/promises";
import process from "node:process";
The error you will definitely see
SyntaxError: Cannot use import statement outside a module means Node read a .js file as
CommonJS. Fix it by adding "type": "module" to package.json (Day 29), or by renaming the file
to .mjs. Do not "fix" it by switching to require().
Code that waits: promises and await
Reading a file is the first thing you have done that takes time. Memory access is instant on a human scale; a disk is not, and a network is far worse. JavaScript's answer shapes everything you write from here on, so meet it properly now.
JavaScript does not freeze while it waits. It starts the slow work, carries on with whatever else it can do, and comes back when the result is ready. To make that possible, a slow function does not return the value — it returns a promise: an object standing in for a value that is not here yet. A promise ends in one of two states, fulfilled with a value or rejected with an error.
A promise is a conversion-complete flag
You do not halt the whole microcontroller waiting on an ADC. You start the conversion, let the
processor do other work, and act when the conversion-complete flag is raised. A promise is that
flag plus the reading it will carry; await is the code that waits on it.
The coat-check ticket
Hand over your coat and you get a ticket immediately. The ticket is not the coat — it is a claim on one. You can walk around holding it, and when you present it later you get the coat back, or you get told it was lost.
The keyword await waits for a promise to settle and gives you the plain value:
const text = await readFile("equipment.json", "utf8");
Without await, text would hold the promise object itself rather than the file's contents —
the single most common mistake with this code.
await is only allowed inside a function marked async, or at the top level of an ES module.
The second part is why the line above works directly in your file today: you set
"type": "module", so top-level await is available. Any async function returns a promise,
which is why callers of your own async functions must await them too.
A rejected promise behaves like a thrown error, so try/catch handles it — which is exactly how
the walkthrough deals with a missing file:
try {
const text = await readFile(path, "utf8");
} catch (error) {
// runs when the promise rejects, e.g. the file does not exist
}
Checkpoint
You should be able to say what a promise is, what await does to one, where await is allowed,
and what you get back if you forget it.
The filesystem, arguments, and the environment
Three server-side capabilities cover most of what you need today.
Reading files. node:fs/promises gives file operations that return promises, so you can
await them, exactly as above.
The "utf8" argument asks for a string. Leave it out and you get a Buffer — raw bytes — which
JSON.parse cannot read. Note that readFile returns text; turning it into data is still your
job, with JSON.parse.
Arguments. process.argv is an array of the words typed on the command line. Position 0 is
the Node executable, position 1 is your script, and your own arguments start at position 2.
const path = process.argv[2] ?? "equipment.json";
Exit codes. Day 4 taught that every command finishes with a number: 0 for success, anything
else for failure. Setting process.exitCode = 1 makes your script report failure, so scripts and CI
that run it can tell something went wrong. (process.env holds environment variables; you will use
it for configuration in later weeks.)
Errors from the filesystem carry a machine-readable code property. The one you must recognise is
ENOENT — "error: no such entry" — meaning the file does not exist.
Walkthrough: a maintenance report script
Make a folder, and inside it a package.json so Node treats .js files as ES modules. (This file
uses .mjs anyway, so it would work either way — but get in the habit.)
mkdir -p ~/fullstack-journey/node-basics && cd ~/fullstack-journey/node-basics
Create equipment.json:
[
{ "id": "eq-001", "name": "Feed Pump 3", "serial": "FP3-2019-114", "status": "operational" },
{ "id": "eq-002", "name": "Cooling Fan A", "serial": "CFA-2021-008", "status": "down" },
{ "id": "eq-003", "name": "Bench PSU", "serial": "PSU-2020-441", "status": "maintenance" }
]
Now report.mjs. Read it in three parts: validate, load, summarise.
import { readFile } from "node:fs/promises";
import process from "node:process";
const VALID_STATUSES = ["operational", "maintenance", "down"];
function validate(records) {
if (!Array.isArray(records)) {
throw new Error("expected the file to contain an array of equipment records");
}
records.forEach((record, index) => {
for (const field of ["id", "name", "serial", "status"]) {
if (typeof record[field] !== "string") {
throw new Error(`record ${index}: "${field}" is missing or not a string`);
}
}
if (!VALID_STATUSES.includes(record.status)) {
throw new Error(
`record ${index}: status "${record.status}" is not one of ${VALID_STATUSES.join(", ")}`
);
}
});
return records;
}
validate never trusts the file. A file on disk is input from outside your program — someone
may have hand-edited it — so it gets checked exactly like a form submission.
async function main() {
const path = process.argv[2] ?? "equipment.json";
let text;
try {
text = await readFile(path, "utf8");
} catch (error) {
if (error.code === "ENOENT") {
console.error(`Cannot read "${path}": file not found.`);
} else {
console.error(`Cannot read "${path}": ${error.message}`);
}
process.exitCode = 1;
return;
}
let records;
try {
records = validate(JSON.parse(text));
} catch (error) {
console.error(`Invalid data in "${path}": ${error.message}`);
process.exitCode = 1;
return;
}
const counts = { operational: 0, maintenance: 0, down: 0 };
for (const record of records) counts[record.status] += 1;
console.log(`Maintenance summary for ${path}`);
console.log(` total equipment: ${records.length}`);
for (const status of VALID_STATUSES) {
console.log(` ${status}: ${counts[status]}`);
}
}
main();
Two try/catch blocks, deliberately separate: one for "could not read the file", one for "the
contents are wrong". They are different failures and deserve different messages. Errors go to
console.error (stderr, from Day 4), not console.log, so a caller can capture the report and
still see problems.
node report.mjs
Maintenance summary for equipment.json
total equipment: 3
operational: 1
maintenance: 1
down: 1
Now break it on purpose:
node report.mjs nope.json
echo $?
Cannot read "nope.json": file not found.
1
A readable message and a non-zero exit code. Compare that with what raw Node prints if you delete
the try/catch: a fifteen-line stack trace ending in ENOENT: no such file or directory.
Checkpoint
Say what process.argv[2] holds, why "utf8" is passed to readFile, and why the exit code
matters even though the message already printed.
Your turn
Build the deliverable: a command-line report with clear errors for missing files.
Create
~/fullstack-journey/node-basics/withequipment.jsonandreport.mjsas above. Type the code rather than pasting it.Run
node report.mjs. Confirm the summary matches your data.Add a fourth record to the JSON and re-run. The totals must change.
Run
node report.mjs nope.jsonand check both the message andecho $?.Create
bad.jsoncontaining[{"id":"eq-004","name":"Lathe","serial":"L-1","status":"broken"}]and runnode report.mjs bad.json. You should see:Invalid data in "bad.json": record 0: status "broken" is not one of operational, maintenance, downCreate
junk.jsoncontaining the single line{ oopsand run it. The message comes fromJSON.parse, so its exact wording depends on your Node version — record what yours says.Add one more validation rule of your own (for example,
serialmust be at least four characters) and prove it fires.
Reviewer mode — after your script works
"Review this Node script for browser-only assumptions and weak file error handling." A review must produce specific, actionable findings with evidence — a named line and a concrete failure — not general praise and not a rewrite. Reject any answer that hands you a new file instead of naming defects, and check each finding yourself before changing anything.
Common pitfalls
Cannot use import statement outside a module. A.jsfile read as CommonJS. Add"type": "module"topackage.jsonor use the.mjsextension.- Forgetting
await.readFilereturns a promise. Withoutawaityou handJSON.parseaPromiseobject and get a confusing type error rather than a file error. - Letting a raw
ENOENTstack trace be the user experience. It is real evidence, but for a tool other people run, catch it and say which path failed. - Trusting the file because it is on your own disk. A hand-edited JSON file is untrusted input.
This is the same discipline as Day 32's
unknown, and Day 40 applies it to HTTP request bodies.
Verify it yourself
Open today's reference, the Node.js Introduction, and find where it describes what Node is and what it provides.
- Does Node's own introduction agree that Node uses the V8 engine? Find the sentence.
- This lesson claimed the browser has no filesystem access equivalent to
node:fs. Find where the Node docs describe the module and note one function it offers that this lesson did not mention. - Find Node's documented rule for when a
.jsfile is treated as an ES module. Does it match the"type": "module"rule stated above?
Write the three answers as comments at the bottom of report.mjs. Tomorrow you replace the file
with a network socket and the same script shape becomes a server.
The hour
- 0–5 min Recall
- 5–20 min Learn
Read only the listed concept notes and official reference sections needed today.
- 20–48 min Build
Write a Node script that reads a JSON file, validates it, and prints a maintenance summary.
- 48–55 min Explain and verify
Run the result, inspect evidence, and explain the data/control flow in your own words.
- 55–60 min Quiz and commit
Complete the quiz, record one lesson, and commit the verified change when applicable.
What to hand in
A command-line report with clear errors for missing files.
Working with AI today
Provide existing work and ask for concrete defects, risks, missing tests, and unsupported assumptions—not praise.
Review this Node script for browser-only assumptions and weak file error handling.
References
End-of-day quiz
Explain-back gate
Pass the quiz above to unlock completion.
Quiz + explain-back checks required.