Backend Development 8 min read

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.

Backend DevelopmentNode.jsCommonJSES 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

login 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.