Introduction
If you're a Node.js developer, you've probably heard ofcjs
andesm
modules but may be unsure why there's two and how do these coexist in Node.js applications. This blogpost will briefly walk you through the history of JavaScript modules in Node.js (with examples 🙂) so you can feel more confident when dealing with these concepts.
The global scope
Initially JavaScript only had a global scope were all members were declared. This was problematic when sharing code because two independent files may use the same name for a member. For example:
greet-1.js
functiongreet(name){return`Hello${name}!`;}
greet-2.js
vargreet="...";
index.html
<!DOCTYPE html><html><head><metacharset="utf-8"><title>Collision example</title></head><body><!-- After this script, `greet` is a function --><scriptsrc="greet-1.js"></script><!-- After this script, `greet` is a string --><scriptsrc="greet-2.js"></script><script>// TypeError: "greet" is not a functiongreet();</script></body></html>
CommonJS modules
Node.js formally introduced the concept of JavaScript modules withCommonJS (also known ascjs
). This solved the collision problem of shared global scopes since developers could decide what to export (viamodule.exports) and import (viarequire()). For example:
src/greet.js
// this remains "private"constGREETING_PREFIX="Hello";// this will be exportedfunctiongreet(name){return`${GREETING_PREFIX}${name}!`;}// `exports` is a shortcut to `module.exports`exports.greet=greet;
src/main.js
// notice the `.js` suffix is missingconst{greet}=require("./greet");// logs: Hello Alice!console.log(greet("Alice"));
npm packages
Node.js development exploded in popularity thanks tonpm packages which allowed developers to publish and consume re-usable JavaScript code.npm
packages get installed in anode_modules folder by default. Thepackage.json file present in allnpm
packages is especially important because it can indicate Node.js which file is the entry point via the"main" property. For example:
node_modules/greeter/package.json
{"name":"greeter","main":"./entry-point.js"//...}
node_modules/greeter/entry-point.js
module.exports={greet(name){return`Hello${name}!`;}};
src/main.js
// notice there's no relative path (e.g. `./`)const{greet}=require("greeter");// logs: Hello Bob!console.log(greet("Bob"));
Bundlers
npm
packages dramatically sped up the productivity of developers by being able to leverage other developers' work. However, it had a major disadvantage:cjs
was not compatible with web browsers. To solve this problem, the concept of bundlers was born.browserify was the first bundler which essentially worked by traversing an entry point and "bundling" all therequire()
-ed code into a single.js
file compatible with web browsers. As time went on, other bundlers with additional features and differentiators were introduced. Most notablywebpack,parcel,rollup,esbuild andvite (in chronological order).
ECMAScript modules
As Node.js andcjs
modules became mainstream, theECMAScript specification maintainers decided to include themodule concept. This is why native JavaScript modules are also known as ESModules oresm
(short for ECMAScript modules).
esm
defines new keywords and syntax for exporting and importing members as well as introduces new concepts like default export. Over time,esm
modules gained new capabilities likedynamic import() andtop-level await. For example:
src/greet.js
// this remains "private"constGREETING_PREFIX="Hello";// this will be exportedexportfunctiongreet(name){return`${GREETING_PREFIX}${name}!`;}
src/part.js
// default export: new conceptexportdefaultfunctionpart(name){return`Goodbye${name}!`;}
src/main.js
// notice the `.js` suffix is requiredimportpartfrom"./part.js";// dynamic import: new capability// top-level await: new capabilityconst{greet}=awaitimport("./greet.js");// logs: Hello Alice!console.log(greet("Alice"));// logs: Bye Bob!console.log(part("Bob"));
Over time,esm
became widely adopted by developers thanks to bundlers and languages likeTypeScript since they are capable of transformingesm
syntax intocjs
.
Node.js cjs/esm interoperability
Due to growing demand, Node.js officially added support foresm
in version12.x
. Backwards compatibility withcjs
was achieved as follows:
- Node.js interprets
.js
files ascjs
modules unless thepackage.json
sets the"type" property to"module"
. - Node.js interprets
.cjs
files ascjs
modules. - Node.js interprets
.mjs
files asesm
modules.
When it comes tonpm
package compatibility,esm
modules can importnpm
packages withcjs
andesm
entry points. However, the opposite comes with some caveats. Take the following example:
node_modules/cjs/package.json
{"name":"cjs","main":"./entry.js"}
node_modules/cjs/entry.js
module.exports={value:"cjs"};
node_modules/esm/package.json
{"name":"esm","type":"module","main":"./entry.js"}
node_modules/esm/entry.js
exportdefault{value:"esm"};
The following runs just fine:
src/main.mjs
importcjsfrom"cjs";importesmfrom"esm";// logs: { value: 'cjs' }console.log(cjs);// logs: { value: 'esm' }console.log(esm);
However, the following fails to run:
src/main.cjs
// OKconstcjs=require("cjs");// Error [ERR_REQUIRE_ESM]:// require() of ES Module (...)/node_modules/esm/entry.js// from (...)/src/main.cjs not supportedconstesm=require("esm");console.log(cjs);console.log(esm);
The reason why this is not allowed is becauseesm
modules allow top-levelawait
whereas therequire()
function is synchronous. The code could be re-written to use dynamicimport()
, but since it returns aPromise
it forces to have something like the following:
src/main.cjs
(async()=>{const{default:cjs}=awaitimport("cjs");const{default:esm}=awaitimport("esm");// logs: { value: 'cjs' }console.log(cjs);// logs: { value: 'esm' }console.log(esm);})();
To mitigate this compatibility problem, somenpm
packages expose bothcjs
andmjs
entry points by leveragingpackage.json
's"exports" property withconditional exports. For example:
node_modules/esm/entry.cjs
:
// usually this would be auto-generated by a toolmodule.exports={value:"esm"};
node_modules/esm/package.json
:
{"name":"esm","type":"module","main":"./entry.cjs","exports":{"import":"./entry.js","require":"./entry.cjs"}}
Notice how"main"
points to thecjs
version for backwards compatibility with Node.js versions that do not support the"exports"
property.
Conclusion
That's (almost) all you need to know aboutcjs
andesm
modules (as of Dec/2024 🙃). Let me know your thoughts below!
Top comments(1)

- Joined
Thankyou so much enjoy your video very good with helping me understand you are so clever, my self find it hard because of the letter writing more with colour code I have dyslexia.
For further actions, you may consider blocking this person and/orreporting abuse