Node.js official docs are excellent at explaining the two different types of module systems. CommonJS and ESM.
There is a very good chapter in Node.js Design Patterns, Chapter 2: The Module System (2020) that describes where CommonJS came from. It emerges from the need to provide a module system for JavaScript in browserless environments simply because one did not exist, other than the AMD and UMD initiatives, and to not rely on urls and <script> tags for resources. It was successful so became popular.
We now have ECMAScript Modules thanks to the ES2015 official proposal and it attempts to align the management of modules in both server-side and browser environments.
The main points to help you decide are:
- CommonJS uses the
require function to load modules and is synchronous. As a result, when you assign a module to module.exports for example, it too must be synchronous. If you have asynchronous stages in your module then you can export an uninitialized module but using require will mean that there is a chance it won't be ready to use in the code that requires it before it's initialized. An example of exporting and importing might look like this:
// titleCase.cjs
function titleCase(str) {
if (!str){
return '';
}
return str.toLowerCase().split(' ').map(word => {
if (!word){
return word;
}else{
return word.charAt(0).toUpperCase() + word.slice(1);
}
}).join(' ');
}
module.exports = titleCase;
// app.js
const titleCase = require('./titleCase');
console.log(titleCase('the good, the bad and the ugly'));
// "The Good, The Bad And The Ugly"
- ES modules use the
import keyword to load modules and the export keyword to export them. They are static, so they need to be described at the top level of every module and support loading modules asynchronously. ES modules facilitate static analysis of the dependency tree, making dead code elimination (tree shaking) more efficient. The same titleCase function above using ESM would look like:
// titleCase.js
export default function titleCase(str) {
if (!str){
return '';
}
return str.toLowerCase().split(' ').map(word => {
if (!word){
return word;
}else{
return word.charAt(0).toUpperCase() + word.slice(1);
}
}).join(' ');
}
// app.js
import titleCase from './titleCase.js'; //< File extension required on import
console.log(titleCase('the good, the bad and the ugly'));
// "The Good, The Bad And The Ugly"
- ES modules run implicitly in strict mode (can't be disabled). This is a good thing in many opinions as it enforces good coding practices.
- In CommonJS you can use the helpful
__filename and __dirname. In ES modules you need to do a workaround to get the same functionality like so:
import { fileURLToPath } from 'url'
import { dirname } from 'path'
const __filename = fileURLToPath(import.meta.url)
const __dirname = dirname(__filename)
- You can dynamically load modules in CommonJS with
require:
let usefulModule;
if (process.env.DEV === true) {
usefulModule = require('./utils/devmodule')
} else {
usefulModule = require('./utils/prodmodule')
}
- And in ESM by using
import as function:
let lib;
if (process.env.DEV === true) {
lib = await import('./utils/devmodule.js');
} else {
lib = await import('./utils/prodmodule.js');
}
const usefulModule = lib.usefulModule;
One thing I have noticed over the years of programming is that languages and libraries evolve. I have observed a growing shift in Node package docs giving more and more of their examples in ESM format. CommonJS is still the dominant option on npm but it tells me the ecosystem is slowly shifting away from CommonJS and towards ESM. Maybe they will run in tandem but such is the nature of evolution, one becomes dominant over the others. All of the projects I work on use the ESM approach. Good luck deciding.