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.
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.
360 Tech Engineering
Official tech channel of 360, building the most professional technology aggregation platform for the brand.
How this landed with the community
Was this worth your time?
0 Comments
Thoughtful readers leave field notes, pushback, and hard-won operational detail here.