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.
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:
| File | Module System |
|---|---|
.mjs | Always ESM |
.cjs | Always CommonJS |
.js | Depends 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 usesimport.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, useimport(orimport.meta.resolvefor 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.