Previously, I wrote about various types of modules in Node.js and JavaScript. I briefly mentioned CommonJS, AMD, and ESM modules, which you can review in the articles "Understanding require in Node.js" and "Understanding modules in Node.js. Why are there so many module types?". However, I didn't go into depth about them.
Many people are unsure when to use require
and when to use import
. Can you use both in the same project? In today's article, let's explore how these two types of modules work in Node.js to answer these questions.
It can be said that the lack of a clear module system in JavaScript from the beginning has led to the complexity we face today. Early JavaScript communities had to create various module systems for it, including AMD, UMD, and more. When Node.js emerged, it chose CommonJS as its default module system.
Realizing its shortcomings, ECMAScript eventually introduced the official module system for JavaScript, which is ESM. JavaScript was immediately updated with this module type, but perhaps it was a bit late because there were already many packages created using unofficial module systems. Nevertheless, whether sooner or later, it's only a matter of time until ESM becomes widespread.
In summary, CommonJS was born for use in Node.js, while ESM is the official module system of JavaScript. However, ESM is only supported from Node.js 12+ onwards, while in modern browsers, ESM can generally run. Can browsers run CommonJS? The answer is no. But we can still create packages that can run in both environments.
So can the browser run all ESM modules? The answer is still no. Simply put, if a module contains Node.js-specific functions that the browser lacks, it won't run. Conversely, ESM may not necessarily run in Node.js because it may contain functions not supported by Node.js. In the end, whether it runs or not depends on the developer's support.
For example, creating a module with CommonJS:
// add.js file
// or add.cjs file
function add(a, b) {
return a + b;
}
module.exports = add;
The .cjs
extension is entirely valid and is used to indicate to Node.js that this is a module using CommonJS.
Then we can use the module using require
syntax:
const add = require('./add.js')
add(1, 2);
Similarly, an ESM module will look like this:
// add.js file
// or add.mjs file
function add(a, b) {
return a + b;
}
export default add;
Similarly, .mjs
is also a valid extension and indicates that this is an ESM module.
Use ESM module using import
syntax:
import add from './add.js';
add(1, 2);
So, the most significant difference between CommonJS and ESM lies in the module import/export syntax and the use of import
and require
to use them.
The answer is yes. You can follow the instructions below.
It's straightforward, use the import
syntax as usual:
// index.js
import add from './add.cjs';
add(1, 2);
Because require
is a synchronous function, it cannot be used to import ESM modules. Instead, to import ESM into CommonJS, we will use import
. import returns a Promise
, and the result contains a default
field pointing to the default export of the ESM module.
// index.js
(async function () {
const add = (await import('./index.mjs')).default;
add(1, 2);
})();
Since await
must be called within an async function, this writing style is necessary. Alternatively, you can use a newer version of Node.js that supports Top-level await.
Sometimes you might find projects using both require
and import
syntax side by side. This is likely due to the use of some transpilation tools like TypeScript, Webpack, Rollup, etc. In essence, we can freely write require
or import
syntax, but after building, the tool will convert the code to a consistent CommonJS or ESM syntax according to preconfigured settings.
Universal modules refer to modules that can work in both Node.js and browsers. This means depending on the usage context, they will automatically use either CommonJS or ESM syntax.
To achieve this, it's simple. Declare the file paths used in each environment in the package.json
file.
{
...
"main": "cjs/index.js",
"module": "es6/index.js",
...
}
With main
pointing to index.js
using CommonJS syntax and module
pointing to index.js
using ESM syntax. When you install and use that package in either Node.js or a browser environment, it will automatically understand and choose the compatible module type. This way, you can write code compatible with both browsers and Node.js in the same package.
"But wait, do I have to write code twice in two places?" You can, but hardly anyone does that because build tools help us create Universal modules. You write the code once, and through a build step, it generates code for both environments.
There are many build tools you can find, such as Webpack, Rollup, and a relatively new and fast one called esbuild.
Readers can refer to the documentation of the tool they want to use or search for examples and frameworks to get started quickly on GitHub.
CommonJS works in Node.js but not in browsers. ESM is supported by all modern browsers and the latest versions of Node.js 12+.
Many tools in the JavaScript ecosystem were developed in Node.js, and Node.js has only recently begun to support ESM. Therefore, most projects still use CommonJS.
If you're starting a new Node.js project and are unsure whether to fully support ESM or not, consider the fact that many common npm packages still use CommonJS. Although ESM can potentially support importing modules using CommonJS, it's still worth considering potential issues and troubleshooting them later.
In the end, Universal is a concept that refers to packages that can work in various environments such as Node and browsers. By declaring file paths to import files in package.json
, it instructs the environment on how to import compatible module types.
References:
The secret stack of Blog
As a developer, are you curious about the technology secrets or the technical debts of this blog? All secrets will be revealed in the article below. What are you waiting for, click now!
Subscribe to receive new article notifications
Comments (3)