CommonJS in the wild

Beginner35 min

Read older require and module.exports code, see how it differs from ES modules, load one kind from the other, and convert a CommonJS file to ESM.

What you will learn
  • Read code that uses require and module.exports
  • List the differences between CommonJS and ES modules
  • Import CommonJS from ESM and load ESM with require
  • Convert a small CommonJS module to ES modules
On this page

Node.js had a module system years before JavaScript did. It is called CommonJS (CJS), and a large amount of existing code, tutorials and packages still use it. You should write new code as ES modules, but you will read CommonJS often, and sometimes need the two to work together.

What CommonJS looks like

A CommonJS module sets module.exports to whatever it wants to share. Another file loads it with require().

legacy_math.cjs
function add(a, b) {
return a + b;
}
function square(n) {
return n * n;
}
module.exports = { add, square, label: "legacy" };

The program below also loads one ES module, so create a third file first. It is explained in the section on using ESM from CommonJS, further down.

modern.mjs
export function double(n) {
return n * 2;
}
export default "modern default";
app.cjs
const path = require("node:path");
const { add, square } = require("./legacy_math.cjs");
const modern = require("./modern.mjs");
console.log(add(2, 3), square(4));
console.log(path.basename(__filename));
console.log(require.main === module);
console.log(typeof exports, typeof module);
console.log(modern.double(21));
console.log(modern.default);
Output
5 16
app.cjs
true
object object
42
modern default

Save these three files (legacy_math.cjs, modern.mjs and app.cjs) in one folder and run node app.cjs. The .cjs extension always means CommonJS. A .js file is CommonJS when the nearest package.json says "type": "commonjs" (which npm init writes) and also, by default, when there is no "type" at all. In that last case Node 22.7 and later switches to ES modules if the file turns out to contain import or export, as the first lesson of this module described. Do not count on that.

  • require("./legacy_math.cjs") runs the file and returns its module.exports. Destructuring picks names out of it.
  • require("node:path") loads a built-in module.
  • __filename is the path of the current file. __dirname is its folder. ES modules do not have them; use import.meta.filename and import.meta.dirname.
  • require.main === module is true when this file is the one that was started, and false when another file loaded it.
  • exports and module are variables Node.js gives every CommonJS file. They are not globals.

ESM compared with CommonJS

ES modules CommonJS
Syntax import and export require() and module.exports
Loading static imports are analysed before running require() runs when the line is reached
Strict mode always only if you write "use strict"
Top-level await allowed not allowed
Exports live bindings a plain object; a value taken out of it is a copy
Needs a file extension yes no for require("./file"), which also tries .js
__dirname, __filename, require not available available
this at the top level undefined module.exports

Because ESM imports are static, tools and engines can see the structure of the program before it runs. ESM is also the module system of browsers. For those reasons, new Node.js code should be ESM.

A common mistake with exports: exports starts as another name for module.exports. Adding a property (exports.add = add) works. Assigning a new value to it (exports = { add }) only changes the local variable and exports nothing. Assign to module.exports when you want to replace the whole export.

Using CommonJS from ESM

An ES module can import a CommonJS file. The whole module.exports is the default import. Node.js also tries to detect named exports by reading the CommonJS source, as a convenience, so { add } often works as well. For reliable code, import the default and take names from it.

esm_side.mjs
import { createRequire } from "node:module";
import legacy, { add } from "./legacy_math.cjs";
console.log(legacy.label, add(1, 2), legacy.square(3));
const require = createRequire(import.meta.url);
const again = require("./legacy_math.cjs");
console.log(again === legacy);
Output
legacy 3 9
true

createRequire builds a require function for use inside an ES module. It needs a location to resolve relative paths from, and import.meta.url provides it. The result is the same object as the default import, so again === legacy is true.

Using ESM from CommonJS

CommonJS code can load an ES module with require(), as app.cjs did with modern.mjs. This works without a flag in Node.js 22.12 and later, and does not print a warning from 22.13 (it is on by default in 23.0). In Node.js 25.4 it stopped being marked experimental. The ES module’s named exports become properties of the returned object, and its default export is the default property, which is why the example printed modern.default.

There is one limit: require() is synchronous, so it cannot load an ES module that uses top-level await. Node.js throws ERR_REQUIRE_ASYNC_MODULE in that case. Such a module must be loaded with import(), which returns a promise and works everywhere.

Converting CommonJS to ESM

The steps are mechanical.

  1. Rename the file to .mjs, or set "type": "module" in package.json.
  2. Replace module.exports = { a, b } with export { a, b }, or put export before each declaration.
  3. Replace const { a } = require("./file.cjs") with import { a } from "./file.mjs". Add the file extension.
  4. Replace __dirname and __filename with import.meta.dirname and import.meta.filename.
  5. Check for require() calls inside functions or conditions. Static import cannot do this; use import().

Common pitfalls

  • Using require in a .mjs file. It is not defined. Use import, or createRequire.
  • Using import in a file that is definitely CommonJS. In a .cjs file, or a .js file under "type": "commonjs", import ... from is a syntax error. A dynamic import() call is allowed. In a .js file with no "type" at all, Node 22.7 and later retries it as an ES module, which hides the mistake until the file is run somewhere else.
  • Using top-level await in a CommonJS file. It is a syntax error.
  • Writing exports = .... It exports nothing. Use module.exports = ....
  • Mixing both styles in one file. Pick one per file.
  • Forgetting the extension in ESM. import "./shapes" fails.
  • Assuming named imports from CommonJS always work. They depend on a heuristic. The default import always works.

Exercises

ExerciseConvert a CommonJS module to ESM

Convert these two CommonJS files into ES modules that print the same output.

shapes_cjs.cjs
const PI = 3.14159;
function circleArea(r) {
return PI * r * r;
}
function rectArea(w, h) {
return w * h;
}
module.exports = { circleArea, rectArea };
use_shapes_cjs.cjs
const { circleArea, rectArea } = require("./shapes_cjs.cjs");
console.log(circleArea(2).toFixed(2));
console.log(rectArea(3, 4));
Output
12.57
12
Show solution

The module file becomes shapes_esm.mjs: export goes in front of each function you share, and module.exports disappears.

shapes_esm.mjs
const PI = 3.14159;
export function circleArea(r) {
return PI * r * r;
}
export function rectArea(w, h) {
return w * h;
}

The consumer swaps require for import and adds the extension.

use_shapes_esm.mjs
import { circleArea, rectArea } from "./shapes_esm.mjs";
console.log(circleArea(2).toFixed(2));
console.log(rectArea(3, 4));
Output
12.57
12

Both versions print 12.57 and 12.

ExerciseRepair an export

This module is meant to export a next function, but the program that uses it crashes with TypeError: counter.next is not a function. Find the one line to change.

counter_broken.cjs
let count = 0;
function next() {
count += 1;
return count;
}
exports = { next };
use_counter_broken.cjs
const counter = require("./counter_broken.cjs");
console.log(counter.next(), counter.next());

Expected output after the fix:

Expected output
1 2
Show solution

exports starts as another name for module.exports. Assigning a new object to exports only rebinds that local name, so module.exports stays an empty object and counter.next is undefined. Assign to module.exports instead. The corrected module is saved here as counter_fixed.cjs, with a user that loads it:

counter_fixed.cjs
let count = 0;
function next() {
count += 1;
return count;
}
module.exports = { next };
use_counter_fixed.cjs
const counter = require("./counter_fixed.cjs");
console.log(counter.next(), counter.next());
Output
1 2

Summary

  • CommonJS uses require() and module.exports. .cjs files, and .js files under "type": "commonjs", are CommonJS. A .js file with no "type" is CommonJS unless Node’s syntax detection finds import or export in it.
  • ESM is the standard, static, strict system with live bindings and top-level await. Write new code as ESM.
  • ESM can import CommonJS: the default import is module.exports. createRequire gives ESM a require.
  • CommonJS can require() an ES module in Node.js 22.12 and later, unless that module uses top-level await.
  • Converting means changing the exports and imports, adding extensions, and replacing __dirname.

Next: a project that combines everything from the beginner level.