DEV Community

Muhammad Bin Zafar
Muhammad Bin Zafar

Posted on • Edited on

The fuss with ESM vs CJS in Node.js

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:

  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.

import express from 'express' // importing the default export
import {Router, Route} from 'express' // importing named exports
Enter fullscreen mode Exit fullscreen mode

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')
Enter fullscreen mode Exit fullscreen mode

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!
Enter fullscreen mode Exit fullscreen mode

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'
}
Enter fullscreen mode Exit fullscreen mode

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!

Top comments (0)