JS Sketchbooksee JavaScript think ✏️
← back to phase 9

9.3 — Modules in Node: CJS vs ESM

Twice now a note has said “Node has TWO module systems — 9.3 untangles them” (8.1), and 8.6 showed you the mysterious "type": "module" switch. Time to pay the debt: Node speaks both CommonJS (its own invention, 2009) and ES Modules (8.1’s standard, 2015) — and one package.json line decides which your files speak.

After today you can read any Node file cold — old tutorial or new config — without blinking at require.

watch it happen
// ---- CommonJS (Node’s own, 2009) ----
// math.cjs
const TAX = 0.25;
function withTax(p) { return p * (1 + TAX); }
module.exports = { withTax };

// cart.cjs
const { withTax } = require("./math.cjs");
console.log(withTax(100));

// ---- ES Modules (the 8.1 standard) ----
// needs "type": "module" in package.json
import { withTax } from "./math.js";

The timeline explains everything. Node shipped in 2009; the language didn’t get import/export until 2015. Six years of real projects needed SOME way to split files — so Node invented its own system: CommonJS. Not a rival standard by arrogance; a gap-filler by necessity.

2009Node ships — inventsCommonJS2015the language getsimport/exporttodayboth live in Node, side bysidesix years with no official system → Node built its ownthe timeline explains everything: Node shipped in 2009, import/exportarrived in 2015
under the hood

The deeper story, with the real names for things — this part is what turns “I saw it” into “I can explain it.”

Under CommonJS, Node literally wraps every file in a hidden function — function (exports, require, module, __filename, __dirname) { … } — and calls it. That’s why require and module “just exist” in CJS files: they’re parameters (3.1!), not globals. ESM files don’t get this wrapper, which is also why __dirname vanishes there (9.5 shows the replacement).

The deeper difference from 8.1 still applies: ESM imports are live bindings resolved before execution; CJS hands you a snapshot object whenever the require call happens to run. Most days this changes nothing — it matters exactly when you hit circular imports, and then it matters a lot.

ESM in Node also unlocks top-level await (6.6’s await outside any function) — CJS files can’t do that, another reason new projects flip the switch.

Job note: TypeScript adds a twist you’ll meet in Phase 11 — your playwright.config.ts is written with import/export, and the compiler outputs whichever system the project’s settings ask for. You write the standard; tooling handles the costume.

your turn

⌨️ build require yourself

CommonJS is refreshingly unmagical: exports is a plain object, require is a plain function. Prove it by building the whole mechanism in the sandbox.

requirements:

  • Model a module: create moduleBox as an object whose exports property starts as an empty object.
  • Model the module’s sealed file scope with a bare { } block (3.5’s bubble!). Inside it: TAX set to 0.25, withTax as a const arrow function returning price * (1 + TAX), and finally assign moduleBox.exports an object holding withTax.
  • Outside the block, write requireFrom(box): it simply returns the box’s exports. Destructure withTax out of a requireFrom(moduleBox) call (4.11) — the name is free out here, because the block kept the module’s own copy private.
  • Print withTax(100), then print typeof requireFrom(moduleBox) — proof that what require hands back is just an object.

when you press RUN, the console must show exactly:

125
object

✏️ Quick check 1

A file’s top line is: const { test } = require("@playwright/test"). Which module system — CommonJS or ESM?

✏️ Quick check 2

A default Node project (no "type" field in package.json) — which system do its .js files speak?

✏️ Quick check 3

Is require a keyword like import, or a normal function? Type keyword or function.

teach it back

🗣️ Now teach it back

Explain to a friend why Node has two module systems, how to recognize each in one glance, and what "type": "module" in package.json actually does.

Write it as if your friend is sitting next to you. Saved to your journal — future-you will use these notes to teach others.

a few sentences, minimum — you’ve got this
to remember
Node (2009) predates import/export (2015) → it invented CommonJS: module.exports = a plain object; require("./file") = a plain function returning it (run once, cached).
Recognition in one glance: require/module.exports = CJS · import/export = ESM. Node’s .js default is CJS; "type": "module" flips the project to ESM (.cjs/.mjs override per file).
CJS is dynamic (require anywhere) but loses static-graph powers; ESM is static and adds top-level await. ERR_REQUIRE_ESM = CJS trying to require an ESM file — fix with the switch or a rename.