Modules (ESM vs CommonJS)
Modules let you split code into separate files with their own scope, and explicitly share (export) or consume (import/require) functionality between them, instead of relying on global variables.
ES Modules (ESM) — the standard
The modern, standardized module system, used natively in browsers (<script type="module">) and in Node.js (.mjs files, or "type": "module" in package.json).
// math.js
export function add(a, b) {
return a + b;
}
export const PI = 3.14159;
export default function multiply(a, b) {
return a * b;
}
// main.js
import multiply, { add, PI } from "./math.js"; // default import + named imports
console.log(add(2, 3)); // 5
console.log(PI); // 3.14159
console.log(multiply(2, 3)); // 6
import * as math from "./math.js"; // import everything as a namespace object
console.log(math.add(1, 1)); // 2
Key characteristics:
- Static: imports/exports are resolved at parse time (before code runs), which enables tree-shaking (bundlers can strip unused exports).
- Always strict mode, automatically.
- Bindings are live references — if the exporting module updates the value, importers see the update.
- Runs asynchronously when loaded via
<script type="module">, andimport()(dynamic import) returns a Promise for lazy-loading:
button.addEventListener("click", async () => {
const { heavyFunction } = await import("./heavy-module.js"); // loaded on demand
heavyFunction();
});
CommonJS (CJS) — Node’s original system
The module system Node.js used before ESM support landed; still the default in plain .js files without "type": "module".
// math.js
function add(a, b) {
return a + b;
}
module.exports = { add, PI: 3.14159 };
// main.js
const { add, PI } = require("./math.js");
console.log(add(2, 3)); // 5
console.log(PI); // 3.14159
Key characteristics:
- Dynamic/synchronous:
require()is a normal function call, executed at runtime — you can call it conditionally, inside anif, etc. - Exports are a copy of the value at the time of
require()(for primitives) — no live-binding like ESM. - No native browser support — needs a bundler (Webpack, esbuild, etc.) to run in the browser.
Quick comparison
// ESM: static, top-level only, live bindings, async-capable
import { add } from "./math.js";
// CommonJS: dynamic, works anywhere, synchronous, snapshot values
const { add } = require("./math.js");
Node.js today supports both, but a file’s extension (or the "type" field in package.json) determines which system Node uses to parse it.