CommonJS in the wild
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().
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.
export function double(n) { return n * 2;}
export default "modern default";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);5 16app.cjstrueobject object42modern defaultSave 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 itsmodule.exports. Destructuring picks names out of it.require("node:path")loads a built-in module.__filenameis the path of the current file.__dirnameis its folder. ES modules do not have them; useimport.meta.filenameandimport.meta.dirname.require.main === moduleistruewhen this file is the one that was started, andfalsewhen another file loaded it.exportsandmoduleare 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.
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);legacy 3 9truecreateRequire 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.
- Rename the file to
.mjs, or set"type": "module"inpackage.json. - Replace
module.exports = { a, b }withexport { a, b }, or putexportbefore each declaration. - Replace
const { a } = require("./file.cjs")withimport { a } from "./file.mjs". Add the file extension. - Replace
__dirnameand__filenamewithimport.meta.dirnameandimport.meta.filename. - Check for
require()calls inside functions or conditions. Staticimportcannot do this; useimport().
Common pitfalls
- Using
requirein a.mjsfile. It is not defined. Useimport, orcreateRequire. - Using
importin a file that is definitely CommonJS. In a.cjsfile, or a.jsfile under"type": "commonjs",import ... fromis a syntax error. A dynamicimport()call is allowed. In a.jsfile 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
awaitin a CommonJS file. It is a syntax error. - Writing
exports = .... It exports nothing. Usemodule.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
Convert these two CommonJS files into ES modules that print the same output.
const PI = 3.14159;
function circleArea(r) { return PI * r * r;}
function rectArea(w, h) { return w * h;}
module.exports = { circleArea, rectArea };const { circleArea, rectArea } = require("./shapes_cjs.cjs");
console.log(circleArea(2).toFixed(2));console.log(rectArea(3, 4));12.5712Show solution
The module file becomes shapes_esm.mjs: export goes in front of each function you share, and module.exports disappears.
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.
import { circleArea, rectArea } from "./shapes_esm.mjs";
console.log(circleArea(2).toFixed(2));console.log(rectArea(3, 4));12.5712Both versions print 12.57 and 12.
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.
let count = 0;
function next() { count += 1; return count;}
exports = { next };const counter = require("./counter_broken.cjs");
console.log(counter.next(), counter.next());Expected output after the fix:
1 2Show 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:
let count = 0;
function next() { count += 1; return count;}
module.exports = { next };const counter = require("./counter_fixed.cjs");
console.log(counter.next(), counter.next());1 2Summary
- CommonJS uses
require()andmodule.exports..cjsfiles, and.jsfiles under"type": "commonjs", are CommonJS. A.jsfile with no"type"is CommonJS unless Node’s syntax detection findsimportorexportin 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.createRequiregives ESM arequire. - CommonJS can
require()an ES module in Node.js 22.12 and later, unless that module uses top-levelawait. - Converting means changing the exports and imports, adding extensions, and replacing
__dirname.
Next: a project that combines everything from the beginner level.