Understanding Node.js ES Modules and CommonJS Interoperability

This article explains how Node.js supports ES modules since version 8.5, compares CommonJS and ES module loading behaviors, demonstrates interoperability techniques with code examples, and outlines the practical implications of using the experimental‑modules flag for backend development.

360 Tech Engineering
360 Tech Engineering
360 Tech Engineering
Understanding Node.js ES Modules and CommonJS Interoperability

Since Node 8.5, Node.js has offered experimental support for ES modules. The author tests this feature and uses the article to introduce the Node.js module system, highlighting the need to start Node with the --experimental-modules flag (e.g., node --experimental-modules index.mjs).

Many existing Node packages are written in CommonJS, which leads to compatibility problems when mixing with ES modules. The article therefore examines the differences and relationships between CommonJS and ES modules and shows how to interoperate between them.

Key differences include the timing of module loading (runtime for CommonJS vs. static analysis for ES modules), the way imports are resolved, and caching behavior. CommonJS modules copy module.exports into a new object, so values cached at load time do not reflect later changes, whereas ES module imports are live bindings.

Example code demonstrates this: a CommonJS module

// cjs.js
var inner = 3;
let innerIncrease = () => { inner++; };
module.exports = { inner, innerIncrease };

is required in

// main.js
var mod = require('./cjs');
console.log(mod.inner);
mod.innerIncrease();
console.log(mod.inner);

, which prints 3 twice because inner is cached.

The equivalent ES module

// mjs.mjs
export let inner = 3;
export let innerIncrease = () => { inner++; };

is loaded with

// main.mjs
var mod = require('./mjs.mjs');
console.log(mod.inner);
mod.innerIncrease();
console.log(mod.inner);

, producing 3 then 4 due to live bindings.

The --experimental-modules flag also changes module resolution rules (prioritizing .mjs files), requires .mjs extensions for import/export, disallows require inside .mjs, makes imports asynchronous, and sets top‑level this, arguments, require, module, exports, __filename, and __dirname to undefined.

For interoperability, when an ES module imports a CommonJS module, Node treats module.exports as the default export. Example:

// cjs.js
module.exports = { "Hello": "你好", "World": "世界" };

can be imported in an ES module with

// main.mjs
import cjsExport from './cjs.js';
const { Hello, World } = cjsExport;

(alternatively using import {default as cjsExport} or import * as cjsExport).

Conversely, a CommonJS file can load an ES module (Node ≥ 9.7 with the experimental flag). Example ES module:

// es.mjs
export default { "a": 1, "b": 2 };

and loading code:

// main.mjs
(async _ => {
  const es = await import('./es.mjs');
  console.log(es);
})();

.

As ES modules mature, many libraries provide both .js (CommonJS) and .mjs (ES module) versions, allowing a smoother transition for developers while compilers and bundlers can assist when necessary.

Original Source

Signed-in readers can open the original source through BestHub's protected redirect.

Sign in to view source
Republication Notice

This article has been distilled and summarized from source material, then republished for learning and reference. If you believe it infringes your rights, please contactadmin@besthub.devand we will review it promptly.

Backend DevelopmentCommonJSES ModulesModule Interoperability
360 Tech Engineering
Written by

360 Tech Engineering

Official tech channel of 360, building the most professional technology aggregation platform for the brand.

0 followers
Reader feedback

How this landed with the community

Sign in to like

Rate this article

Was this worth your time?

Sign in to rate
Discussion

0 Comments

Thoughtful readers leave field notes, pushback, and hard-won operational detail here.