The fuss with ESM vs CJS in Node.js
Muhammad Bin Zafar
Posted on October 26, 2023
If you are learning to code in Node.js or have learnt to code in Node.js, then you are familiar with callback hell. However, that's a case solved and closed. The one at hand is the dilemma of whether to write our code in the ECMAScript Module format or the CommonJS one. So, let's talk about this!
The Prelude
Node.js 🐢 started in 2009 with the standard of its time, the CommonJS format. Later the ECMAScript specification picked up pace and garnered more stability. For this, ECMAScript modules were first introduced to Node.js in v8.5.0 in 2017 and it continued to receive improvements over the years.
The default format in Node.js is CommonJS, and it is usually not explicitly mentioned in package.json such as { "type": "commonjs" }
. However, the standard format for code reuse in JavaScript is ESM, which needs to be explicitly mentioned in the package.json like so { "type": "module" }
.
The Clash
Many articles and docs including the Node.js docs talk in length about this. The summary, in my opinion, is the following:
- You can use CommonJS code from ESM.
- You can use ESM code from CommonJS.
Firstly, CommonJS code from ESM. It is dead simple from ESM to import CommonJS code that has a default export or named exports.
import express from 'express' // importing the default export
import {Router, Route} from 'express' // importing named exports
However, it gets tricky when there's no named exports from the CommonJS package, and the default-exported variable is an object. This is tricky because in CJS-to-CJS code reuse, we are accustomed to this convenience. But for CJS-ESM interoperability, this is an obvious bug.
The following is perfectly fine between CJS files:
// common.cjs
module.exports = {
a: 10,
b: 20
}
// main.cjs
const {a,b} = require('./common.js')
However, while importing common.cjs
from ESM, we cannot do named imports of the properties a
and b
.
import {a,b} from './common.cjs' // SyntaxError: Named export 'a' not found.
import common from './common.cjs' // Rather, first import the default
const {a,b} = common // Then, access object properties!
Secondly, ESM code from CommonJS. This is not convenient. Importing ESM code from CommonJS is only done through the async import()
function. Since the ESM package is loaded asynchronously, coding pattern and linear reason-ability is hurt.
const importFileType = import('file-type')
exports.isGzip = async function (filename) {
const fileType = await importFileType
const {ext} = fileType.fileTypeFromFile(filename)
return ext == 'gz'
}
exports.isPng = async function (filename) {
const fileType = await importFileType
const {ext} = fileType.fileTypeFromFile(filename)
return ext == 'png'
}
In the code above, the gist is, only to import an ESM module all dependent functions are now being converted to async. CommonJS does not support top-level awaits for us to do const importFileType = await import('file-type')
. For this inconvenience, you should not write library code in ESM that will be reused.
I disagree with Sindre Sorhus on this in terms of writing library code. However, for writing application code, ESM is quite nice and convenient.
Note that, in the code example above, the file-type
package will be imported only once and the promise is also resolved only once - causing no performance issues.
The Solution
This dilemma has multiple frontiers to solve for. For me, the summary is as follows:
Case | Suggestion | Reason | Example project |
---|---|---|---|
Node.js app | ESM | App code are never reused | An express.js backend server e.g. |
Node.js app that will bundled, minified, and packaged | CJS | Better support for CJS in these tools | A command-line app e.g. |
Library package | CJS | Best for the most swift reuse by both | Express.js |
TypeScript | Same rules | Same cases | Read this. |
Babel/Transpilers | Try not using them. |
Thanks for reading! Found it interesting? Give it a ❤️ or share your thoughts/questions in 💭! Did I miss anything? Let me know in the comments.
Have a great day!
Posted on October 26, 2023
Join Our Newsletter. No Spam, Only the good stuff.
Sign up to receive the latest update from our blog.