Skip to content

Modules: ESM and CommonJS

Node.js supports two module systems that cannot be freely mixed:

FeatureESMCommonJS
Syntaximport / exportrequire() / module.exports
File extension.mjs or .js with "type":"module".cjs or .js without "type":"module"
LoadingStatic, analysed at parse timeDynamic, executed at runtime
Top-level awaitSupportedNot supported
Default in Node 20+Yes (opt-in)Legacy default

CommonJS was the original Node.js module system. It is still widely used in older packages and tooling.

// math.js (CommonJS)
function add(a, b) { return a + b; }
function multiply(a, b) { return a * b; }
module.exports = { add, multiply }; // named exports via object
// OR: module.exports = add; // single default export
// main.js (CommonJS)
const { add, multiply } = require('./math');
console.log(add(2, 3)); // 5
console.log(multiply(4, 5)); // 20

require() is synchronous and loads the file immediately when the line is executed. This means you can require() inside an if block or a function.

ESM is the official JavaScript module standard. It is now the recommended system for new Node.js projects.

// math.mjs (ESM — named exports)
export function add(a, b) { return a + b; }
export function multiply(a, b) { return a * b; }
// utils.mjs (ESM — default export)
export default function formatNumber(n) {
return n.toFixed(2);
}
// main.mjs (ESM — named import)
import { add, multiply } from './math.mjs';
import formatNumber from './utils.mjs';
console.log(add(2, 3)); // 5
console.log(formatNumber(3.14159)); // '3.14'

import statements are static — they must appear at the top of the file and are resolved before any code runs. This enables tree-shaking (dead-code elimination) by bundlers.

Two ways to tell Node.js a file uses ESM:

  1. "type": "module" in package.json — all .js files in that package are treated as ESM. Use .cjs for any file that must stay CommonJS.
  2. .mjs file extension — always treated as ESM regardless of package.json.
{
"name": "my-app",
"type": "module"
}
// Named: export multiple bindings, import with exact names (or rename)
export const PI = 3.14159;
export function area(r) { return PI * r * r; }
import { PI, area } from './circle.mjs';
import { area as circleArea } from './circle.mjs'; // rename
// Default: one main export per file, imported with any name
export default class Logger { /* ... */ }
import Logger from './logger.mjs';
import MyLogger from './logger.mjs'; // same thing, different local name

This snippet runs in the Node.js runtime (module resolution requires Node). It demonstrates how an ES module with named exports works end-to-end using inline dynamic import().

// ESM inline via data: URL — requires Node runtime
const src = `
export function add(a, b) { return a + b; }
export const VERSION = '1.0.0';
`;
const mod = await import('data:text/javascript,' + encodeURIComponent(src));
console.log('add(3, 4):', mod.add(3, 4));
console.log('VERSION:', mod.VERSION);
Node.js

Needs the Node.js runtime — open in StackBlitz to run.

What does adding `"type": "module"` to `package.json` do?
Which statement about `import` is true compared to `require()`?
How do you export a single main value from an ESM file?