16 / 20 · Day 6
Day 6 · Concept 16

ES modules vs CommonJS

ES modules (import / export) are the standard. CommonJS (require / module.exports) is legacy Node. Both still in widespread use; modern code uses ESM.


1 · ES modules — the modern way

js math.js
// math.js
export function add(a, b) { return a + b; }
export const PI = 3.14159;

// Default export — one per file
export default function multiply(a, b) { return a * b; }
js main.js
// main.js
import multiply, { add, PI } from "./math.js";

console.log(add(2, 3));
console.log(PI);
console.log(multiply(4, 5));

// Rename
import { add as plus } from "./math.js";

// Import everything as a namespace
import * as math from "./math.js";
console.log(math.add(1, 2));

2 · CommonJS — older Node

js math.cjs
// math.cjs (or default in older Node)
function add(a, b) { return a + b; }
module.exports = { add };

// Or single export
module.exports = function multiply(a, b) { return a * b; };
js main.cjs
const { add } = require("./math.cjs");
const multiply = require("./multiply.cjs");
console.log(add(2, 3));
console.log(multiply(4, 5));

3 · ESM vs CJS — the practical differences

ESMCommonJS
Static — imports resolved at parse timeDynamic — require runs anywhere
Async by designSynchronous
Top-level await worksDoesn't work
Tree-shakable — bundlers can drop unused exportsHard to tree-shake
Requires .mjs or "type": "module" in package.jsonDefault in older Node
__dirname / __filename not built-inBuilt-in

4 · package.json — the switch

json package.json
{
  "name": "myapp",
  "version": "1.0.0",
  "type": "module",
  "main": "index.js",
  "scripts": {
    "start": "node index.js"
  },
  "exports": {
    ".": "./index.js",
    "./utils": "./src/utils.js"
  }
}

"type": "module" makes .js files ESM by default. Without it, they're CJS. "exports" defines the public entry points consumers can import.

5 · Dynamic import — anywhere

js dynamic.js
async function maybeLoad() {
    if (process.env.DEBUG) {
        const { logger } = await import("./debug-logger.js");
        logger.start();
    }
}

Works in both ESM and CJS. Returns a promise that resolves to the module. Useful for code-splitting and conditional loads.

6 · Common mistakes

  • Mixing ESM and CJS without thought. Importing CJS from ESM works; the reverse is awkward.
  • Forgetting the .js extension in ESM imports. import "./util" fails; import "./util.js" works.
  • Top-level await in CJS. Doesn't work. Wrap in an IIFE.

7 · When it clicks

  • You start new projects as ESM by default.
  • You translate require to import automatically.
  • You use "exports" in libraries to control the public surface.
Found this useful?