The fuss with ESM vs CJS in Node.js
Tags: webdev, javascript, beginners, node
Published: 2023-10-26 06:13:10

> 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](https://www.ecma-international.org/publications-and-standards/standards/ecma-262/) 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](https://nodejs.org/api/esm.html) talk in length about this. The summary, in my opinion, is the following:
1. You can use CommonJS code from ESM.
2. 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_.
```js
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:
```js
// 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`.
```js
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.
```js
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](https://gist.github.com/sindresorhus/a39789f98801d908bbc7ff3ecc99d99c) 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.](https://github.com/midnqp/nodejs-backend)
Node.js app that will bundled, minified, and [packaged](https://dev.to/midnqp/bundling-nodejs-into-single-executable-binary-l3g) | CJS | Better support for CJS in these tools | A command-line app [e.g.](https://github.com/midnqp/postman-cli)
Library package | CJS | Best for the most swift reuse by both | [Express.js](https://github.com/expressjs/express)
TypeScript | Same rules | Same cases | Read [this.](https://www.typescriptlang.org/docs/handbook/modules/guides/choosing-compiler-options.html)
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!