Engrain by ByteRush

JS.10.02 Mid-level· 11 minCore — asked often

CommonJS vs ES modules: require, import and making them work together

Why require and import behave differently, how Node.js decides which one a file uses, require(esm) in current Node.js, and how dual packages use the exports field.

Comes up in: JavaScript, Backend design

Learn#

Sana’s Node.js server breaks after she upgrades a library: Error [ERR_REQUIRE_ESM] on older Node.js, or a missing default export on newer ones. The library moved from CommonJS to ES modules.

CommonJS is Node’s original module system: require() loads and runs a file synchronously and returns its module.exports object. The values you read from it are plain property reads, not live bindings.

ES modules are static and may load asynchronously: the graph is built before code runs (JS.10.01). Interop means making the two systems load each other.

cat > counter.cjs <<'JS'
let count = 0;
module.exports = { count, increment() { count++; } };
JS
cat > price.mjs <<'JS'
export default function price(n) { return "₹" + n; }
export const currency = "INR";
JS
cat > main.cjs <<'JS'
const counter = require("./counter.cjs");
counter.increment();
console.log("CommonJS count:", counter.count);
const esm = require("./price.mjs");
console.log(esm.default(99), esm.currency);
JS
node main.cjs
Output (verified)
CommonJS count: 0
₹99 INR

count stayed 0: it was copied into the exports object once. And require of an ES module returned its namespace, with the default under .default.

How Node.js decides a file's module systemStep 1 of 3

State

  • file = app.js

Node.js meets app.js. It looks for the nearest package.json.

Read the steps as text
  1. Node.js meets app.js. It looks for the nearest package.json.
  2. package.json says "type": "module", so .js files in this package are ES modules.
  3. A .cjs file is always CommonJS and a .mjs file is always ESM, whatever package.json says.

Predict#

Predict first

A CommonJS file calls require('./data.mjs'), and data.mjs uses top-level await. What happens in current Node.js?

Interview angle#

Say it like this — about 30 seconds

“CommonJS require is synchronous, can be called anywhere, and returns the module.exports object, so values are not live. ES modules are static, always strict, and their imports are live bindings. Node.js picks the system from the file extension, .mjs or .cjs, or else the package.json type field. Current Node.js can require a synchronous ES module, but not one with top-level await. Libraries ship both formats through the exports field.”

Stays on your device. Nothing is uploaded.
Follow-up Can ESM import CommonJS?
Yes. module.exports becomes the default import, and Node.js detects many named exports by reading the source.
Follow-up What is the dual package hazard?
Shipping both formats can load two copies of one library, so instanceof checks and shared state break.
Follow-up What is __esModule?
A flag that transpilers add to CommonJS output, so code knows exports.default is the real default export.

Practice#

On your own

Write the rule Node.js uses to pick a module system, and the default-import rule bundlers use.

Done when

  • .mjs is always “esm” and .cjs is always “commonjs”.
  • .js follows packageType, and is “commonjs” when the type field is missing.
  • getDefault returns mod.default only when __esModule is true.
Your solution
Ctrl/⌘ + Enter

Remember#

Try to answer each card before you open it. Then add them to your reviews.

What does require() return?

The module's module.exports object, loaded synchronously.

How does Node.js treat a .js file when package.json has "type": "module"?

As an ES module.

Which ES modules can require() load in current Node.js?

Synchronous ones, with no top-level await.

What does the exports field in package.json do?

Defines the package's entry points, per condition such as import or require.

Go deeper

Four layers. Stop at the one your interview needs; each ends with a quick check.

The core ideaEvery round

.mjs is always an ES module and .cjs is always CommonJS.
For .js, Node.js reads the nearest package.json: "type": "module" means ESM, and "commonjs" or a missing field means CommonJS.
Recent Node.js also detects ESM syntax in a typeless .js file.

Check yourself: Which file extension forces CommonJS?

.cjs

How it works underneathSenior rounds
require() of a synchronous ES module is unflagged in Node.js 22.12.0 and 20.19.0, and stable from 25.4.0. It returns the module namespace object, with the default export under .default Source: Node.js: Loading ECMAScript modules using require().
Check yourself: In which Node.js versions did require(esm) lose its flag?

22.12.0 and 20.19.0.

Edge cases and trapsWhere people slip

CommonJS has __dirname, __filename and require; ES modules do not. Use import.meta.dirname and import.meta.filename in current Node.js. ESM also needs full file paths with extensions in relative imports: ./price.js, not ./price.

Check yourself: Can CommonJS code use import.meta or top-level await?

No. Both exist only in ES modules.

In productionStaff-level answers
The exports field maps entry points to files per condition: "import", "require", "types", "default". It also hides files you did not list. Dual packages risk two copies of shared state, so many libraries now ship ESM only and rely on require(esm) Source: Node.js: Conditional exports.
Check yourself: Why do packages list a types condition in exports?

So TypeScript finds the right declaration file for each entry point.

Finished the five steps? Mark it, and its cards join your reviews.

Last checked 2026-10-31. Spot a mistake? Use “Report an error” below — fixes are listed on the corrections page.

Interview questions this answers

  • What happens with circular imports in ESM vs CommonJS?