JavaScript module 4: test, structure, ship
Seventeen sessions to turn knowledge into know-how: organising code into modules, testing it with Node's built-in test runner, debugging it methodically, then assembling everything into the final project — a command-line tool that queries the Open-Meteo weather API, summarises the response, and ships tested and documented. Method: 20 minutes of theory, 30 of practice, 10 of recap.
Day 43 — ESM modules: export and import
Goal: split a program into files that import each other, with the modern syntax.
Theory (20 min)
One file per topic, and explicit doors between them: that is the module system. The modern version is called ESM (ECMAScript Modules): export opens a door, import walks through it. It works in a .mjs file, or in .js with "type": "module" in the package.json (day 38).
// maths.mjs
export function double(n) {
return n * 2;
}
export const PI = 3.14159;
export default function triple(n) { // only one default per file
return n * 3;
}
// app.mjs
import triple, { double, PI } from "./maths.mjs";
console.log(double(21), triple(7));
Three practical rules. (1) A named import reuses the export's exact name, between braces; the default export is imported under the name YOU choose. (2) The path is relative to the importing file, and the extension is mandatory in Node (./maths.mjs, not ./maths). (3) The import is static: it happens at the top of the file, once; an imported module executes only once even if ten files import it.
Why this is a boundary and not a convenience: a module hides everything it does not export. The rest of the program can only depend on the doors — change the inside, the doors hold, nothing breaks elsewhere. This is the basis of all team and test splitting: you test a module by its exports.
In import everything, { thing } from "./m.mjs", everything refers to…
- the module's default export
- all exports gathered
- the named export "everything"
Answer
The part outside the braces is always the default export; the braces hold the named exports.
Practice (30 min)
- Create the course's
maths.mjsandapp.mjs, runnode app.mjs: 42 and 21. - Add to maths.mjs a NON-exported
internal()function used by double. Try importing it in app.mjs: read the error, it says the door does not exist. Success criterion: the error is understood and noted, and double still works. - Drop the extension in the import (
"./maths"): read the ERR_MODULE_NOT_FOUND error, fix it. - Migrate one of your module 3 scripts: move the
waitfunction intodelay.mjs, export it, import it. - Three files importing in a chain (a imports b, b imports c): observe that everything resolves at launch, once.
Agent tips: "split this 200-line script into three modules: data, computation, display; list each one's exports BEFORE writing": demand the door plan first. Trap: the agent may mix default and named exports at random. Proof: each import only sees what the plan announced, and the program produces the same output.
Recap (10 min)
Three lines: named vs default export, the relative path and its extension, what a module hides. Without notes: write a utility module with two exports and the file that imports them.
Day 44 — Structuring a project into files
Goal: adopt a simple, durable folder structure for a Node tool.
Theory (20 min)
A project is not one big file: it is a tree whose every level has a role. For a command-line tool, this structure is largely enough:
weather/
├── package.json # identity card, "type": "module"
├── README.md # the manual, first
├── src/
│ ├── index.js # the entry: reads arguments, orchestrates
│ ├── api.js # everything that talks to the network
│ └── summary.js # everything that computes (pure, testable)
└── test/
└── summary.test.js # the tests, next to their subject
The organising principle: separate what talks to the world from what computes. api.js and index.js touch the network, files, the console: those are the effects. summary.js takes data and returns a result, reading and writing nothing: that is the pure core. Tests are written on the pure core — no need to fake the network to test a computation. This boundary is THE architectural decision of the module; day 49 digs deeper.
Two more conventions: the entry point does as little as possible (read argv, call, print, exit code) — sometimes called "the shell"; and the README is drafted early, because writing "how to use it" there reveals design flaws before the code. You will see: a tool that is hard to explain is a badly designed tool.
Practice (30 min)
- Create the course's tree in a new
weatherfolder: npm init -y, "type": "module", the four empty files, the README with three lines of intent. - In
src/summary.js, write a first pure function:export function label(temp) { return temp < 0 ? "freezing" : temp < 15 ? "cold" : temp < 25 ? "mild" : "warm"; } - In
src/index.js, import label and print it for a value passed as an argument (process.argv, Number() — day 37's reflexes). - Success criterion:
node src/index.js 18prints "mild". The architecture runs; the final project will fill it. - In the notebook: for each file of the structure, one line "this file may… / it may not…".
Agent tips: "here is my folder structure and each file's role; my index.js also computes — propose the minimal split to get that out": the agent is good at redistribution. Trap: it may create six folders for three functions — refuse over-structuring. Proof: the entry command produces the same output after the split.
Recap (10 min)
Three lines: effects vs pure core, the entry shell, why the README comes early. Without notes: draw a small CLI's ideal tree from memory.
Day 45 — node:test: your first test
Goal: write and run an automated test with Node's built-in module.
Theory (20 min)
A test is a program that checks another program: it calls your function on chosen inputs and compares the result with what was expected. The value is durable: every time you change the code, one command replays ALL the tests in a second — the fear of breaking disappears, and that fear is what made code untouchable.
Node ships its test runner since v18: node:test plus node:assert. Nothing to install:
// test/summary.test.js
import { test } from "node:test";
import assert from "node:assert/strict";
import { label } from "../src/summary.js";
test("label classifies temperatures", () => {
assert.equal(label(-3), "freezing");
assert.equal(label(10), "cold");
assert.equal(label(20), "mild");
assert.equal(label(30), "warm");
});
Run it with node --test at the project root: it finds the *.test.js files by itself. Anatomy of a test: a name that states the expected BEHAVIOUR (not "test 1"), calls of the function on chosen cases, and assertions. The cases that make a test worth having: the normal case, the boundaries (0, 15 and 25 for label: right at the category changes), and the absurd case. A test that can never fail protects nothing.
Practice (30 min)
- Create the course's test file in your weather project, run
node --test. Success criterion: the output announces 1 test passed, 0 failed. - Read the output in detail: the test's name, the pass/fail counters at the bottom. Remember where the verdict is read.
- Add the boundary tests: label(0), label(15), label(25). They must pass — check on the function that thresholds fall in the expected category.
- Trigger a failure on purpose: change one expectation to "scorching", rerun, READ the failure report (it shows expected vs received), fix it.
- Write the test of a second summary.js function:
export function summariseTemperatures(list)returning{ min, max, average }. Normal case + a one-element array.
Agent tips: "write this function's tests: normal case, boundaries, absurd case" — the agent is a good case supplier. Trap: it may write tests that always pass (an assertion on an evidence). Proof: break the function on purpose; the test MUST go red, otherwise it proved nothing.
Recap (10 min)
Three lines: what a test checks, node --test and its naming conventions, the three case families. Without notes: write the test of day 12's fizzbuzz.
Day 46 — assert: equal, deep, errors
Goal: pick the right assertion: equal, deepEqual, throws, and the strict mode.
Theory (20 min)
node:assert/strict (the /strict matters: it imposes strict equality everywhere) offers four assertions covering the essentials:
assert.equal(a, b): strict equality of simple values (numbers, strings, booleans). For computed decimals, prefer a margin:assert.ok(Math.abs(average - 10.56) < 0.01)— remember 0.1 + 0.2 (day 4).assert.deepEqual(a, b): compares objects and arrays content by content. Two separately built objects are never ===, but their contents can be equal:assert.deepEqual(summariseTemperatures([10, 20]), { min: 10, max: 20, average: 15 }).assert.throws(fn): checks that a function throws — code that refuses bad inputs is tested too.assert.throws(() => convert("abc")). A second argument filters:/not a number/imposes the message.assert.ok(x): x is truthy — for everything the other three do not cover.
The writing rule: an assertion says WHAT, and the failure message alone must let you find the WHY without reopening the test — pick telling data (label(0) beats label(3.7) for a boundary). And a test tests one idea: three assertions on the same behaviour, yes; three behaviours in one test, no — when it fails, you want to know which of the three gave way.
To check that countAll(["a", "a"]) returns the right object, you need…
- assert.equal(result, { a: 2 })
- assert.deepEqual(result, { a: 2 })
Answer
deepEqual: equal compares object references, always different; deepEqual compares contents.
Practice (30 min)
- Take your
countAllfrom day 19 into a module, write its test with deepEqual on three cases, including the empty array (result{}). - Test the float margin: an average function, an expected result within 0.01 using assert.ok and Math.abs.
- Test the error: your
convertfrom day 21, withassert.throws(() => convert("abc"), /not a number/). - Success criterion:
node --testall green; then break each function one by one and watch EACH test go red before repairing — proof that the tests see something. - In the notebook: equal vs deepEqual in one sentence each, with an example from your code.
Agent tips: "these tests use equal on objects — fix to deepEqual and explain when equal suffices": an agent knows the distinction. Trap: it may swap in some variant without saying why — ask for the rule, not just the patch. Proof: a test failing on an almost-equal object (one extra key) demonstrates it.
Recap (10 min)
Three lines: the four assertions and their grounds, the margin for floats, one idea per test. Without notes: test a function returning an array, and one that throws.
Day 47 — Testing asynchronous code
Goal: test async functions, their results and their rejections.
Theory (20 min)
An async test is simply a test whose function is async: the runner awaits the promise it returns.
import { test } from "node:test";
import assert from "node:assert/strict";
test("wait resolves after the delay", async () => {
const start = Date.now();
await wait(50);
assert.ok(Date.now() - start >= 50);
});
test("getJSON rejects on an unreachable server", async () => {
await assert.rejects(() => getJSON("http://localhost:1/never"));
});
Two rules. (1) await everywhere: an async test that forgets to await its promise ends "green" BEFORE the code has finished — the worst test there is, it pretends to check without looking. The symptom: the test passes even when the code is broken. (2) Rejections are checked with assert.rejects, throws' asynchronous sibling: await assert.rejects(fn, /message/).
The immediate follow-up question: "how do I test code that calls a real API?" Answer: you do not, in unit tests. The network is slow, capricious, and changing — a test depending on the Internet fails on a train. The solution is day 44's architecture: the network is isolated in api.js, thin and logic-free; everything that computes is pure and tested on FIXED data written into the test (an API response recorded once, replayed forever). Testing the real network has a name — integration testing — and is done separately, knowingly.
Practice (30 min)
- Write the course's
waittest (take the function from day 31 into a module). Green on first try. - Write a fake
async function lookupCity(name)returning a promise of{ name, latitude, longitude }from a small hard-coded dictionary, rejecting when the city is unknown. Test both paths: result with await + deepEqual, rejection with assert.rejects. - Success criterion: break the dictionary on purpose: the result test goes red. Repair.
- The blind-test trap: write an async test WITHOUT await on a rejected promise; observe it passes (or reports an unhandled rejection) — note the reflex: "an async test without await is suspect".
- Prepare the project: save into
test/fixture-weather.jsona real trimmed Open-Meteo response (current + 3 days of daily forecasts), fetched with your day 35 script. That is your test data for days 52-53.
Agent tips: "write this async function's tests, including the rejection": the agent can. Trap: it may test by calling the REAL network — demand fixed data. Proof: cut the wifi; the tests must stay green.
Recap (10 min)
Three lines: await mandatory in an async test, assert.rejects, why the network stays out of tests. Without notes: write the test of an async function that throws.
Day 48 — Debugging: from console.log to the debugger
Goal: pick the right debugging tool for the size of the problem.
Theory (20 min)
Debugging is not re-reading code while hoping: it is shrinking the gap between what you believed and what happens, measurement after measurement. Three tools, three problem sizes:
- The surgical console.log: for 90% of cases. Not "it doesn't work" sprinkled everywhere: one precise hypothesis ("I think this value is undefined here"), one log just before, one conclusion. The rule: one log = one hypothesis. And console.log removes its own errors when forgotten — remember to remove them.
- The
debugger;statement: placed in the code, it suspends execution when an inspector is attached. Under Node:node --inspect-brk app.js, then openchrome://inspectin a Chromium browser: step by step, variable values at every instant, breakpoints set with a click. For logic knots that resist the log. - The reproducing test (days 45-47): a bug isolated in a test never comes back. Debugging that ends well finishes with a red test turned green.
The method, always the same: (1) reproduce the bug reliably — if you cannot trigger it at will, start there; (2) state ONE hypothesis; (3) test it with a log or a breakpoint; (4) fix, verify, write the test. One hypothesis at a time: whoever changes three things at once will never know which one repaired — and may have broken something else.
Practice (30 min)
- Plant a bug: in your day 26 scores.js, write the sort comparator backwards. Run, observe, then debug ONLY with two hypothesis console.logs. Note the process in the notebook.
- Try the debugger: add
debugger;inside a function, runnode --inspect-brk, open chrome://inspect, step through and inspect two variables. Success criterion: you watched a value change step by step. - The repairing test: write the test that would have caught the sort bug. It is red with the bug, green without. Keep it.
- Timed exercise: an imaginary friend sends you "my average gives NaN". Three plausible causes: empty list, a non-numeric element, division by the wrong number. Write the three reproductions, find which matches their symptom.
Agent tips: "this test fails, here is the code and the output: propose three hypotheses ranked by likelihood, and the log that would settle it": an agent is strong at ordered hypotheses. Trap: it will fix on its own authority if you do not forbid it — you want the diagnosis first. Proof: the hypothesis confirmed by the log run at home.
Recap (10 min)
Three lines: the three tools and their sizes, the one-hypothesis-one-log rule, the test that closes a bug. Without notes: tell the four-step method to your notebook.
Day 49 — Writing testable code: separating computation from effects
Goal: recognise and repair code that is impossible to test without breaking it.
Theory (20 min)
Some functions can be tested in two lines; others require the Internet, a file, and a prayer. The difference is not the tester's talent, it is the code's structure. A pure function always returns the same result for the same inputs and touches nothing outside itself: it is tested for free. A function with effects (network, files, console, randomness, clock) depends on the world: it is tested with difficulty.
// hard to test: computation and effect mixed
async function showWeather(city) {
const data = await getJSON(urlOf(city));
const text = `It is ${data.current.temperature_2m} °C`;
console.log(text);
}
// testable: the computation comes out, the effect stays at the entry
function formatWeather(data) {
return `It is ${data.current.temperature_2m} °C`;
}
async function showWeather(city) {
console.log(formatWeather(await getJSON(urlOf(city))));
}
The pattern has a name: shell and core. The core (formatWeather, your module 2 pipelines) is pure, abundant, and covered with tests. The shell (index.js, api.js) is thin: it reads argv, calls the API, passes the data to the core, prints the result — so thin it is checked by eye. When a function is "impossible to test", the question is not "how do I simulate the world" but "what computes in here, and why has it not been extracted?". The refactoring that pulls the computation out is called… extracting a function: you have known how since day 12.
Practice (30 min)
- Take your day 35 weather script: everything is in one main function. Extract
formatWeather(data)(pure) and leave main as the shell. Same output when run. - Write formatWeather's test on your day 47 fixture — three assertions on the returned text.
- Success criterion: the test passes WITHOUT a network (cut the wifi to be sure) and the script produces the same display as before the split.
- Spot in your old scripts a function mixing console.log and computation: extract the computation, test it.
- A case to discuss in the notebook: does your formatWeather use the current time or randomness? If so, pass them IN AS PARAMETERS — even time and chance become testable when they enter through the door.
Agent tips: "split this function into pure core and shell; do not change any behaviour": a mechanical refactoring the agent does well if you forbid any other touch-up. Trap: the agent renames and "improves" along the way — demand the minimal diff. Proof: identical output, and the core passes a test without a network.
Recap (10 min)
Three lines: pure vs effects, shell and core, time and randomness as parameters. Without notes: turn a function that prints into a function that returns, plus the caller that prints.
Day 50 — Final project 1/6: the weather CLI's specification
Goal: set in writing what the tool will do — before writing a single line.
Theory (20 min)
The final project: a command-line tool that gives a city's weather, summarised. Two network calls (geocoding the city name, then the forecast), a computed summary, a clean display, tests, a README. Six sessions, each one deliverable.
Why a specification for such a small tool? Because projects that drift start drifting in the first hour, not at the end. Your spec fits in eight lines:
- Usage:
node src/index.js <city> [--days N]— N between 1 and 7, default 3. - Output: city, country, then for each day: date, min/max temperatures, weather label (rain, snow…). All aligned and readable.
- APIs: geocoding
https://geocoding-api.open-meteo.com/v1/search?name=…&count=1then forecasthttps://api.open-meteo.com/v1/forecast?…&daily=temperature_2m_max,temperature_2m_min,weathercode&timezone=auto— free, no key. - Errors: city not found, network down, missing argument → a clear message, exit 1.
- Tests: all the computation (formatting, summary, labels) tested on a fixture; the network out of the tests.
- Deliverable: code split into modules, green tests, a README that lets a stranger use the tool.
Note what the spec does NOT contain: no colours, no interactive mode, no cache. A finished project beats an ambitious one: every "wouldn't it be nice" goes on a "later" list, not into the code.
Practice (30 min)
- Write the specification at the top of your README.md, in YOUR words and with your choices (message language, default number of days).
- Explore both APIs by hand with curl or the browser: geocode your city, note the structure (
results[0].latitude,.longitude,.country); call the forecast, note thatdailyholds PARALLEL arrays (daily.time[i] goes with daily.temperature_2m_max[i]). - Success criterion: your notebook holds the two complete working URLs for your city, and the exact path of each useful field.
- Build the fixture: save a real forecast response (3 days are enough) into
test/fixture-weather.json. - List the modules: api.js (the two calls), summary.js (labels, formatting — pure), index.js (shell). One export per planned function, written as a comment.
Agent tips: "review this specification and list the edge cases I forgot": the agent is a good spec reviewer. Trap: it will propose ten extra features — they go into "later", not into the spec. Proof: every added edge case has its answer in writing (exact message, exit code).
Recap (10 min)
Three lines: what the spec fixes, what it refuses, the structure of the two APIs' responses. Without notes: recite your tool's exact usage as if presenting it.
Day 51 — Final project 2/6: querying Open-Meteo
Goal: write api.js: geocoding, forecasts, clean errors.
Theory (20 min)
Today, the network shell. Two functions, each short, each with its errors:
import { URLSearchParams } from "node:url";
export async function geocode(city) {
const params = new URLSearchParams({ name: city, count: "1" });
const data = await getJSON(`https://geocoding-api.open-meteo.com/v1/search?${params}`);
if (!data.results || data.results.length === 0) {
throw new Error(`city not found: ${city}`);
}
const { latitude, longitude, name, country } = data.results[0];
return { latitude, longitude, name, country };
}
export async function forecast({ latitude, longitude }, days) {
const params = new URLSearchParams({
latitude: String(latitude),
longitude: String(longitude),
daily: "temperature_2m_max,temperature_2m_min,weathercode",
forecast_days: String(days),
timezone: "auto"
});
return getJSON(`https://api.open-meteo.com/v1/forecast?${params}`);
}
Plus your day 36 getJSON (the ok check, the 5 s AbortController) shared at the top of the same file. Three points of care: the destructuring at geocode's exit (day 20) only forwards what is needed — downstream no longer depends on the API's shape; ALL parameters go through URLSearchParams, never glued by hand (a city name with a space would break the URL); and each function states clearly what can make it fail.
api.js stays out of node:test — it is the network shell. We check it by hand, once, on real cases: that is the accepted price of the shell/core architecture.
Practice (30 min)
- Write api.js in full (getJSON + geocode + forecast). Test geocode by hand in a throwaway script: your city, then
"Zzzztown"for the not-found error. - Test forecast on your city's coordinates: the response holds daily.time of the right length for --days.
- Success criterion: the three cases (known city, absurd city, wifi off) each give a comprehensible message or error — no raw stack.
- Check the URLSearchParams trap: geocode a city with spaces ("New York"). Without proper encoding, it broke; here, it passes.
- Note in the notebook the exact contract: what geocode returns (four fields), what forecast returns (the raw response).
Agent tips: "review my api.js: any unencoded parameter, any silent error case?": a targeted review, two questions only. Trap: the agent may rewrite getJSON "better" and break your timeout. Proof: the three manual cases pass after any touch-up.
Recap (10 min)
Three lines: geocode's contract, why URLSearchParams everywhere, why api.js escapes unit tests. Without notes: rewrite geocode from memory.
Day 52 — Final project 3/6: summarising the data
Goal: write summary.js, the pure core: weather labels and report formatting.
Theory (20 min)
The pure core takes the API's response and returns text. Two problems to solve cleanly. The first: Open-Meteo's weathercodes are numbers (0 = clear sky, 61 = light rain, 95 = thunderstorm…): a lookup-table object does the job, with a default for unknown codes — external data is never exhaustive.
const LABELS = {
0: "clear sky", 1: "mostly clear", 2: "partly cloudy", 3: "overcast",
45: "fog", 48: "freezing fog",
51: "light drizzle", 61: "light rain", 63: "rain", 65: "heavy rain",
71: "light snow", 80: "showers", 95: "thunderstorm"
};
export function label(code) {
return LABELS[code] ?? `code ${code}`;
}
The second: parallel arrays. daily.time[i], daily.temperature_2m_max[i] and daily.temperature_2m_min[i] describe the same day: you "zip" them by index with map over time, then compose the report with map + join("\n") — your module 2, on a real mission. The date is formatted with Intl (day 23):
const fmt = new Intl.DateTimeFormat("en-GB", { weekday: "short", day: "numeric", month: "short" });
export function report(response) {
return response.daily.time.map((day, i) => {
const date = fmt.format(new Date(day));
const min = Math.round(response.daily.temperature_2m_min[i]);
const max = Math.round(response.daily.temperature_2m_max[i]);
return `${date}: ${min}° / ${max}°, ${label(response.daily.weathercode[i])}`;
}).join("\n");
}
Pure end to end: same response in, same text out, zero network, zero console. Tomorrow, this file becomes the most tested of the project.
Practice (30 min)
- Write summary.js (LABELS, label, report). The number of weather codes in the table is your choice — cover at least the course's fifteen.
- Try report on your fixture in a throwaway script: three report lines, localised dates, rounded temperatures.
- Success criterion: the displayed report matches the fixture's data, checked day by day by hand; and a weathercode missing from the table prints "code 99" without crashing.
- Add
export function header(place)returning« Weather for {name}, {country} »— tomorrow's title line. - Re-read summary.js aloud: if it contains any fetch, console.log or argument-less new Date()… note that new Date(day) WITH an argument is pure. Check the distinction in the notebook.
Agent tips: "here is the WMO weathercode table, complete my LABELS object": with the table supplied, the agent is reliable. Trap: without the table, it invents plausible but wrong labels. Proof: two or three codes checked in the Open-Meteo documentation.
Recap (10 min)
Three lines: lookup table and default, zipping by index, what makes report pure. Without notes: rewrite label and report's skeleton.
Day 53 — Final project 4/6: the project's tests
Goal: cover the project's core with tests that pass — and that can fail.
Theory (20 min)
Harvest day: summary.js is pure, it tests without a network, on the fixture. The method for writing a test suite that serves:
- The happy path: the real fixture gives the expected report — write the expectation by reading the fixture, not by copying the program's output (otherwise you test that the program agrees… with itself).
- The boundaries: a single day, negative temperatures, an unknown code.
- Non-regression: every bug met during the project becomes a test.
import { test } from "node:test";
import assert from "node:assert/strict";
import { readFile } from "node:fs/promises";
import { label, report } from "../src/summary.js";
const fixture = JSON.parse(await readFile(new URL("./fixture-weather.json", import.meta.url), "utf8"));
test("report renders one line per day", () => {
const lines = report(fixture).split("\n");
assert.equal(lines.length, fixture.daily.time.length);
});
test("label translates and survives the unknown", () => {
assert.equal(label(0), "clear sky");
assert.equal(label(999), "code 999");
});
Loading the fixture deserves an explanation: import.meta.url is the test file's URL; new URL("./fixture-weather.json", import.meta.url) derives the fixture's path relative to the test — reliable whatever the launch folder (day 39's trap, defused). Top-level await is legal in a module.
Practice (30 min)
- Write
test/summary.test.js: the course's two tests, plus the content test (one line contains "° /" and a label known to be in the fixture). - Run
node --test. Success criterion: all green, wifi off. - Add the "single day" boundary: hand-build a mini-response inside the test, check the single line.
- Perform the proof ritual: break label (always return "rain"), watch the tests go red, repair.
- Also test header from day 52, and label-the-temperature if you kept it. Count your tests in the notebook: six to ten makes a good suite for this project.
Agent tips: "here are my fixture and my summary.js; propose the missing tests, citing the fixture line that justifies each expectation": forcing it to cite the data kills invented expectations. Trap: a test written from the program's output tests nothing. Proof: the break-it-on-purpose ritual turns every test red.
Recap (10 min)
Three lines: the three test families, the expectation read from the fixture, import.meta.url for paths. Without notes: write a complete test of label.
Day 54 — Final project 5/6: errors, --help and README
Goal: finish the shell: arguments, error messages, usage documentation.
Theory (20 min)
The index.js shell assembles: read the arguments, call api.js, pass the data to summary.js, print, and translate any error into a human message + exit 1. It fits on one screen:
import { geocode, forecast } from "./api.js";
import { header, report } from "./summary.js";
const USAGE = "usage: node src/index.js <city> [--days N]";
async function main() {
const args = process.argv.slice(2);
if (args.length === 0 || args.includes("--help")) {
console.log(USAGE);
process.exit(args.length === 0 ? 1 : 0);
}
const iDays = args.indexOf("--days");
const days = iDays === -1 ? 3 : Number(args[iDays + 1]);
if (!Number.isInteger(days) || days < 1 || days > 7) {
throw new Error("--days expects an integer between 1 and 7");
}
const city = args.filter(a => !a.startsWith("--")).filter(a => a !== String(days))[0];
const place = await geocode(city);
const response = await forecast(place, days);
console.log(header(place));
console.log(report(response));
}
main().catch(err => {
console.error(`error: ${err.message}`);
process.exit(1);
});
Two conventions of a well-raised tool: --help answers the usage and exits with code 0 (asking for help is not a failure; a missing argument is); and errors go to console.error (the error output) so they do not mix with the result when the output is redirected. The README completes it: one presentation sentence, the install (Node LTS, nothing else), three usage examples, a three-line "how it works" section.
Practice (30 min)
- Write index.js in full. Run: your city,
--days 5,--help, no argument, absurd city,--days 12. Six runs, six planned behaviours. - Success criterion: each error prints ONE clear line and the exit code is 1 (check with
echo $?), each success prints the report. - Test the redirect:
node src/index.js Lyon > weather.txt— the file holds ONLY the report; errors would go to the terminal. - Write the README.md: presentation, prerequisites, usage with two real examples (one-word city, city with spaces), a three-line "architecture" section (api/summary/index), and how to run the tests.
- The stranger test: hand the README for review — to yourself in two days, or to an agent — and check it is enough to use the tool.
Agent tips: "review my README and play the user: which questions go unanswered?": the agent is a good documentation guinea pig. Trap: it will also answer the questions "for you" — anything worth knowing goes into the README. Proof: the list of questions it raised, and your answers merged in.
Recap (10 min)
Three lines: the exit 0/1 contract and --help, console.error vs console.log, a tool README's minimal contents. Without notes: recite the six verification runs.
Day 55 — Final project 6/6: review and presentation
Goal: re-read your own code like someone else's, and ship.
Theory (20 min)
The project's last skill is not technical: it is the cold review. Your code, read with a rested head, as if it came from a colleague you neither want to hurt nor to fool. The reading grid for a CLI of this size:
- Each function: a name that tells the truth, a single job, under one screen?
- Each file: in its place in the shell/core split?
- Errors: does every throw carry a message useful to whoever will suffer it?
- The dead: commented-out code, unused function, debugging console.log — out.
- Names: would a stranger understand
data,r,tmp? Renaming is the most profitable refactoring there is.
Then the presentation: three minutes aloud (or in the notebook) — what the tool does, one live run, the file map, one difficulty and how you solved it. Whoever can present their tool owns it; whoever stumbles has found their next working session.
Practice (30 min)
- Re-read each file with the course's grid, pencil in hand. Every anomaly becomes an immediate fix or a "later" line in the README.
- Re-run the whole suite:
node --test. Success criterion: green after the review — the renamings broke nothing, the tests proved it. - Re-run day 54's six scenarios after the review: behaviour has not moved.
- Write the three-minute presentation in the notebook, time yourself aloud.
- Package: the weather folder holds src/, test/, README.md, package.json — nothing superfluous. You have shipped a tool.
Agent tips: "review src/ against this grid: [paste the grid]. Flag, do not fix": review-without-rewrite is the setting that makes the agent useful here. Trap: its tastes are not yours — you rule on each remark, not it. Proof: tests and the six scenarios stay green after applying WHAT you retained.
Recap (10 min)
Three lines: the review grid, what you fixed, what the presentation revealed. The project is shipped — four days of consolidation and the path is complete.
Day 56 — Deno, Bun, nvm: the ecosystem beyond Node
Goal: place Node's alternatives and version managers, without reinstalling anything.
Theory (20 min)
Node is not alone, and you will read these names everywhere:
- Deno (deno.com): created by Node's original author, it fixes its youthful flaws — TypeScript with no configuration, security by default (network and files require an explicit permission), modules by URL. A real candidate for your next personal tools.
- Bun (bun.sh): the sprinter — very fast startup and package installs, an integrated toolbox (tests, bundler). Node-compatible for the essentials.
- Version managers (
nvm,fnm, or your system's package manager): several Node versions on one machine, one per project. The day a project requires Node 20 and another Node 24, you will know it is a solved problem.
The honest advice: change nothing now. Node LTS covers this whole path and most of the working world. These alternatives will be worth testing when you have a need they solve — a TypeScript tool, a project that starts a thousand times a day. The JavaScript ecosystem moves fast; your defence is not to follow everything, but to recognise a real need from a fashion.
Practice (30 min)
- Check your installation:
node --version,npm --version. Note them in the notebook with the date. - Without installing anything, run one of your scripts with the "permission" mindset: list what it touches (network? files?) — that is the question Deno asks explicitly, and it already applies to Node.
- Read Deno's and Bun's homepages (5 min each): note one promise of each, and what it would change for your weather CLI.
- Success criterion: you can answer in two sentences "why Node and not Deno for this path?" — the answer lies in ecosystem maturity and ubiquity, not chance.
- In the notebook: your personal rule "when do I switch tools?" in one sentence.
Agent tips: "my tool needs X; does Node LTS do it natively today?": the question that defuses 80% of useless migrations. Trap: agents love new tools — demand the comparison with YOUR need, not the list of novelties. Proof: the feature's official doc page.
Recap (10 min)
Three lines: Deno and its permissions, Bun and its speed, your migration rule. Without notes: name today's three families of tools.
Day 57 — Guided review of the whole path
Goal: redo the four modules' founding moves, from memory.
Theory (20 min)
No new notion. The path fits in ten moves; today you redo them all, without notes, in the console or Node. It is the dress rehearsal before tomorrow's mock exam.
- Declare let/const and pick the right one (D3).
- Handle strings and template literals (D5).
- Read inside a nested object, dot and brackets (D7).
- Write a function, classic and arrow forms (D12).
- Write a factory closure (D16).
- Compose filter/map/reduce (D17-19, 25).
- Destructure with a default (D20).
- Predict the synchronous / microtask / task order (D30).
- Write an async function with try…catch and getJSON (D33, 36).
- Write a node:test test with boundaries (D45-46).
Every move that resists points back to its session: re-read the theory, redo the practice. Two days remain to consolidate them — that is exactly what they are for.
Practice (30 min)
- Time the ten moves: each in the console or a throwaway file, from memory. Note each one's time in the notebook.
- Success criterion: eight moves out of ten done without help. Below that, the faulty moves are redone tomorrow before the exam.
- For the two slowest moves: redo them a second time immediately — the second pass anchors the first.
- Re-read your notebook from A to Z: highlight the three sentences you quote most often. Those are your assets; the rest is on its way.
Agent tips: "here is my list of ten moves; quiz me on them, one question at a time, and wait for my answer before correcting": the agent-examiner, set to patience. Trap: it will correct too fast if you do not spell it out. Proof: you produced each answer BEFORE reading its.
Recap (10 min)
Three lines: the solid moves, the moves to redo tomorrow, your best progress of the path. Tomorrow: the mock exam, one hour, no notes.
Day 58 — The mock exam
Goal: prove the whole path in one hour, without notes or help.
Theory (20 min)
One hour, timed, no notebook, no documentation, no AI. Four short papers covering the four modules. It is not a trap: everything in it has been done before. If a paper blocks you for more than ten minutes, note it and move to the next — the list of blockages IS the exam's result.
- Language (15 min): write
analyse(text)returning{ words, longest, vowels }: the word count, the longest word, the vowel count (a e i o u y). Test it by hand on two sentences. - Functions (10 min): write
afterDelay(value, ms)returning a promise fulfilling with the value after ms; then an async function that uses it to add two values received at different times. - Node (15 min): a script
stats.mjsthat reads a JSON file of numbers (create it) and prints min, max, average — with a clean message if the file is missing. - Tests (20 min):
analyse's test suite: normal case, empty string, single word, and a test that would have caught a forgotten uppercase ("É" is not in your vowel list — whether you handle it is your choice, but the test documents the choice).
Practice (30 min)
- Set a 60-minute timer and do the four papers in a row. Silence, phone away.
- Success criterion: the three programs run correctly and the test suite is green — within the hour, or with the extra time noted.
- After the timer, self-marking in the notebook: for each paper, "fluent", "laborious but done", "blocked" — and the blockage's cause in one line.
- Every "blocked" becomes a session to redo this week, BEFORE considering the path finished.
Agent tips: resist the agent during the exam — it is the one day it has no place. AFTERWARDS, though: "here are my four productions and my self-criticisms; what did I miss?" is an excellent review. Proof: its remarks only count if you reproduce or refute them by running code.
Recap (10 min)
Three lines: the score per paper, each blockage's cause, the sessions to redo. Tomorrow: the review, and the rest of your life with JavaScript.
Day 59 — Path review and what comes next
Goal: close the path properly and pick the next mountain.
Theory (20 min)
Fifty-nine sessions. You started from an empty console; you finish with a command-line tool that talks to a real API, structured in modules, covered with tests, documented. Re-read the checklist on the path's home page and tick honestly: every ticked box is an asset, every empty box is a program — not a reproach.
The natural continuations, by taste:
- Towards the web: you know the language; the DOM and interfaces are the next step — the site's web development path is made for that, and everything you learned applies as-is.
- Towards servers: your day 41 mini-API holds the door open; a framework (Express, Fastify) and a database are the next two notions.
- Towards another language: the site's Python path will now go very fast — the ideas (functions, tests, modules) are the same, only the grammar changes.
- Towards open source: your weather CLI deserves a public repository, a polished README, and maybe its first users.
The last lesson is the first day's: one hour a day beats seven hours on Sunday. JavaScript never ends — it is practised. Pick a project that serves you, and carry on.
Practice (30 min)
- Tick the checklist on the path's home page, looking at your js/ folder as evidence.
- Re-read your notebook from day 1 to day 59: it fits in your hand and tells a progression — it is the path's hidden deliverable.
- The final success criterion: you present the weather CLI (three minutes, day 55) to someone — a relative, a colleague, a recording. Aloud, for real.
- Write in the notebook the next step you choose, and the first concrete session (what, when, one hour).
- Put the project's "later" items in a visible list: that is your personal backlog, the material of the coming weeks.
Agent tips: "here is my CLI and its README; draft the repository description (one sentence) and three keywords": the agent excels at presenting what exists. Trap: it will oversell — your tool does three things, not thirty. Proof: the sentence describes exactly what node src/index.js Lyon produces.
Recap (10 min)
Three lines: what you can do that you could not 59 days ago, the next step chosen, your first session scheduled. The path is over; the practice begins. Congratulations.
Found this useful? Tell us what to measure next.
Comments
No account, no tracker: a comment is read by a human before it appears.