ESM vs CommonJS

Two Module Systems, Same Node Process

ESM vs CommonJS

Node has two module systems — the legacy CommonJS (`require`) and modern ESM (`import`). Know which you're in.

4 min read Level 2/5 #nodejs#esm#cjs
What you'll learn
  • Recognize CJS and ESM syntax
  • Switch a project between them
  • Interop between the two

Node has two module systems. Both are everywhere in the wild.

CommonJS — The Legacy

// math.js
function add(a, b) { return a + b; }
const PI = 3.14159;
module.exports = { add, PI };
// app.js
const { add, PI } = require("./math");

console.log(add(2, 3));

Notice: require() is a function, module.exports is an assignment, no .js extension needed. This is the original Node module system.

ESM — The Modern

Same as browser JS, plus extension required:

// math.mjs
export function add(a, b) { return a + b; }
export const PI = 3.14159;
// app.mjs
import { add, PI } from "./math.mjs";

How Node Decides

Per file:

FileModule System
.mjsAlways ESM
.cjsAlways CommonJS
.jsDepends on package.json "type"

In package.json:

{ "type": "module" }

Then .js files in that project are ESM. Without it (or with "type": "commonjs"), .js is CJS.

For new projects: set "type": "module". ESM is the future and matches the rest of the JS ecosystem.

Interop

ESM can import CJS:

// works — Node creates a default export for module.exports
import lodash from "lodash";    // lodash is a CJS package
console.log(lodash.chunk([1,2,3,4], 2));

CJS importing ESM is harder — you usually use await import():

// .cjs file
async function run() {
  const mod = await import("./esm-only.mjs");
  mod.doThing();
}
run();

Differences That Bite

  • __dirname, __filename — CJS only. ESM uses import.meta.url:
import { fileURLToPath } from "node:url";
import { dirname } from "node:path";

const __filename = fileURLToPath(import.meta.url);
const __dirname  = dirname(__filename);
  • require() — CJS only in scripts. In ESM, use import (or import.meta.resolve for paths).
  • Top-level await — ESM only. CJS doesn’t allow it.

When to Use Each

  • New code → ESM. Set "type": "module".
  • Libraries published to npm → publish both (dual package) if practical.
  • Old codebase → migrate gradually; CJS isn’t going away.
`package.json` →