Understanding CommonJS and ESM in Javascript

CommonJS and ESM (or ES6 modules) are two different ways of importing modules in javascript with ESM being the recent addition to the JavaScript language specification and are seeking to unify and standardize how modules are loaded in JavaScript applications.

This introduces a major problem for using ES modules with Node because ESM has to find a way to live alongside CommonJS which is already a widely used module system.

This boils down to the fact that ESM is asynchronously loaded, while CommonJS is synchronous. However, Let's keep it simple for now and simply figure out how to use both of them independently and together.

We are going to use a simple module that does math addition and subtraction for samples

Sample CommonJS Approach (create a folder named commonJs)

file commonJS/module.js

const addition = (x, y) => x + y;
const subtraction = (x, y) => x - y;

module.exports = {
  addition, subtraction
}

file commonJS/index.js

const { addition, subtraction} = require('./module');

console.log(addition(1,1));
console.log(subtraction(1,1));

Sample ESM Approach (create a folder named esm)

file esm/module.js

export function addition(x, y) {
  return x + y;
};
export function subtraction(x, y) {
  return x - y;
};

file esm/index.js

import {addition, subtraction} from './module.js';

console.log(addition(1,1));
console.log(subtraction(1,1));

Now there are 3 major differences

  1. CommonJS uses module.exports and require syntax while ESM uses import/export syntax.
  2. For ESM, type of module has to be specified in the package.json i.e
    {
    "name": "esm",
    "type": "module", // attention to this
    "scripts": {
     "start": "node index"
     }
    }
    
    however, that can be ignored for CommonJs because CommonJs is the default; or specified as "type": "commonjs".
  3. This is a hidden detail that I want you to figure out on the import and require lines (because this causes a lot of headaches if mixed up). The answer is number 1 in the Key Takeaway section.

Now that we have understood these, the next challenge is how to use the two in a single project.

but WHY?

CommonJs has been available for a while now as mentioned earlier, so most third-party modules are in CommonJs format, hence most nodejs developers opt for CommonJs. However, what if you need a few other modules that are written in ESM, how will this be handled in a project with a single package.json?

There are 2 ways: use CommonJs and require both ESM and CommonJs or use ESM and import both.

First I will like to clarify that requiring ESM in CommonJS is very tricky and introduces some level of complexity so what I mostly do is once I have some ESM modules to be imported, I just stick to ESM and not CommonJs. After all, it is what the future of javascript is aimed at for standardization and unification.

Tho it needs to be pointed out that most libraries/modules are still written in CommonJS so existing codebase developers that use commonJs don't have to do a lot of work to migrate their entire code into ESM because of just a few new libraries they need to work with).

Notwithstanding, I will only focus on Importing CommonJS in ESM for this article which will enable the use of a solution (ESM) for both types of modules.

Compatibility: Importing CommonJS in ESM

ESM fully supports both types of modules using the import syntax which implies that you can import both Common JS modules (module.exports syntax) and (export syntax).

Let us create a folder named commonjs-in-esm and try to import both modules from the last section.

import { addition as addCommonJs, subtraction as subCommonJs } from '../commonJs/module.js';
import { addition as addESM, subtraction as subESM } from '../esm/module.js';

// commonjs
console.log(addCommonJs(1,1));
console.log(subCommonJs(1,1));

// esm
console.log(addESM(1,1));
console.log(subESM(1,1));

This implies that ESM is fully compatible with both itself and CommonJS.

Remember this is an ESM import so the type of module needs to be specified in package.json after you run npm init

{
  "name": "commonjs-in-esm",
  "type": "module",
  "scripts": {
    "start": "node index"
  }
}

Now, we see that with ESM, we can import both commonJS and esm modules which is a good step towards standardization and unification for modules in Javascript.

Key Takeaways

  1. You probably didn't notice this but whenever you use import statement, you have to add the file extension to it eg

    import {addition, subtraction} from './module.js'; // pay attention to **.js**
    

    as opposed to require statement which doesn't need it

    const { addition, subtraction} = require('./module');
    
  2. If you are an application author, and your Node.js app is written as ESM, then everything is probably going to be just fine! You can easily import both CommonJS and ESM dependencies as explained in the Compatibility section.

  3. If you are a library author and want to allow your consumers to still use CommonJS, It is easiest to just stick with CommonJS entirely. However, If you want to provide both CommonJS and ESM files, then consider writing ESM code and transpiling it to CommonJS for publishing.

You can find the full code for this piece @ github.com/abdulloooh/js-modules-hasnode

Other useful Resources

CommonJS and ESM import/export compatibility, by example

Understanding ES6 Modules (Import / Export Syntax) in JavaScript

Getting Started with (and Surviving) Node.js ESM

Using ECMAScript modules (ESM) with Node.js

How to use ESM on the web and in Node.js