DEV Community

Cover image for CommonJS vs. ES modules in Node.js
Matt Angelosanto for LogRocket

Posted on • Originally published at blog.logrocket.com

CommonJS vs. ES modules in Node.js

Written by Kingsley Ubah ✏️

In modern software development, modules organize software code into self-contained chunks that together make up a larger, more complex application.

In the browser JavaScript ecosystem, the use of JavaScript modules depends on the import and export statements; these statements load and export EMCAScript modules (or ES modules), respectively.

The ES module format is the official standard format to package JavaScript code for reuse and most modern web browsers natively support the modules.

Node.js, however, supports the CommonJS module format by default. CommonJS modules load using require(), and variables and functions export from a CommonJS module with module.exports.

The ES module format was introduced in Node.js v8.5.0 as the JavaScript module system was standardized. Being an experimental module, the --experimental-modules flag was required to successfully run an ES module in a Node.js environment.

However, starting with version 13.2.0, Node.js has stable support of ES modules.

This article won’t to cover much on the usage of both module formats, but rather how CommonJS compares to ES modules and why you might want to use one over the other.

Comparing CommonJS modules and ES modules syntax

By default, Node.js treats JavaScript code as CommonJS modules. Because of this, CommonJS modules are characterized by the require() statement for module imports and module.exports for module exports.

For example, this is a CommonJS module that exports two functions:

module.exports.add = function(a, b) {
        return a + b;
} 

module.exports.subtract = function(a, b) {
        return a - b;
} 
Enter fullscreen mode Exit fullscreen mode

We can also import the public functions into another Node.js script using require(), just as we do here:

const {add, subtract} = require('./util')

console.log(add(5, 5)) // 10
console.log(subtract(10, 5)) // 5
Enter fullscreen mode Exit fullscreen mode

If you are looking for a more in-depth tutorial on CommonJS modules, check this out.

On the other hand, library authors can also simply enable ES modules in a Node.js package by changing the file extensions from .js to .mjs.

For example, here's a simple ES module (with an .mjs extension) exporting two functions for public use:

// util.mjs

export function add(a, b) {
        return a + b;
}

export function subtract(a, b) {
        return a - b;
}
Enter fullscreen mode Exit fullscreen mode

We can then import both functions using the import statement:

// app.mjs

import {add, subtract} from './util.js'

console.log(add(5, 5)) // 10
console.log(subtract(10, 5)) // 5
Enter fullscreen mode Exit fullscreen mode

Another way to enable ES modules in your project can be done by adding a "type: module" field inside the nearest package.json file (the same folder as the package you’re making):

{
  "name": "my-library",
  "version": "1.0.0",
  "type": "module",
  // ...
}
Enter fullscreen mode Exit fullscreen mode

With that inclusion, Node.js treats all files inside that package as ES modules, and you won’t have to change the file to .mjs extension. You can learn more about ES modules here.

Alternatively, you can install and set up a transpiler like Babel to compile your ES module syntax down to CommonJS syntax. Projects like React and Vue support ES modules because they use Babel under the hood to compile the code.

Pros and cons of using ES modules and CommonJS modules in Node.js

ES modules are the standard for JavaScript, while CommonJS is the default in Node.js

The ES module format was created to standardize the JavaScript module system. It has become the standard format for encapsulating JavaScript code for reuse.

The CommonJS module system, on the other hand, is built into Node.js. Prior to the introduction of the ES module in Node.js, CommonJS was the standard for Node.js modules. As a result, there are plenty of Node.js libraries and modules written with CommonJS.

For browser support, all major browsers support the ES module syntax and you can use import/export in frameworks like React and Vue.js. These frameworks uses a transpiler like Babel to compile the import/export syntax down to require(), which older Node.js versions natively support.

Apart from being the standard for JavaScript modules, the ES module syntax is also much more readable compared to require(). Web developers who primarily write JavaScript on the client will have no issues working with Node.js modules thanks to the identical syntax.

Node.js support for ES modules

Older Node.js versions don’t support ES modules

While ES modules have become the standard module format in JavaScript, developers should consider that older versions of Node.js lack support (specifically Node.js v9 and under).

In other words, using ES modules render an application incompatible with earlier versions of Node.js that only support CommonJS modules (that is, the require() syntax).

But with the new conditional exports, we can build dual-mode libraries. These are libraries that are composed of the newer ES modules, but they are also backward-compatible with the CommonJS module format supported by older Node.js versions.

In other words, we can build a library that supports both import and require(), allowing us solve the issue of incompatibility.

Consider the following Node.js project:

my-node-library
├── lib/
   ├── browser-lib.js (iife format)
   ├── module-a.js  (commonjs format)
   ├── module-a.mjs  (es6 module format)
   └── private/
       ├── module-b.js
       └── module-b.mjs
├── package.json
└── 
Enter fullscreen mode Exit fullscreen mode

Inside package.json, we can use the exports field to export the public module (module-a) in two different module formats while resticting access to the private module (module-b):

// package.json
{
  "name": "my-library",         
  "exports": {
    ".": {
        "browser": {
          "default": "./lib/browser-module.js"
        }
    },
    "module-a": {
        "import": "./lib/module-a.mjs" 
        "require": "./lib/module-a.js"
    }
  }
}
Enter fullscreen mode Exit fullscreen mode

By providing the following information about our my-library package, we can now use it anywhere it is supported like so:

// For CommonJS 
const moduleA = require('my-library/module-a')

// For ES6 Module
import moduleA from 'my-library/module-a'

// This will not work
const moduleA = require('my-library/lib/module-a')
import moduleA from 'my-awesome-lib/lib/public-module-a'
const moduleB = require('my-library/private/module-b')
import moduleB from 'my-library/private/module-b'
Enter fullscreen mode Exit fullscreen mode

Because of the paths in exports, we can import (and require()) our public modules without specifying absolute paths.

By including paths for .js and .mjs, we can solve the issue of incompatibility; we can map package modules for different environments like the browser and Node.js while restricting access to private modules.

Newer Node.js versions fully support ES modules

In most lower Node.js versions, the ES module is marked as experimental. This means that the module lacks some features and is behind the --experimental-modules flag. Newer versions of Node.js do have stable support for ES modules.

However, it’s important to remember that for Node.js to treat a module as an ES module, one of the following must happen: either the module’s file extension must convert from .js (for CommonJS) to .mjs (for ES modules) or we must set a {"type": "module"} field in the nearest package.json file.

In this case, all code in that package will be treated as ES modules and the import/export statements should be used instead of require().

CommonJS offers flexibility with module imports

In an ES module, the import statement can only be called at the beginning of the file. Calling it anywhere else automatically shifts the expression to the file beginning or can even throw an error.

On the other hand, with require() as a function, gets parsed at runtime. As a result, require() can be called anywhere in the code. You can use it to load modules conditionally or dynamically from if statements, conditional loops, and functions.

For example, you can call require() inside a conditional statement like so:

if(user.length > 0){
   const userDetails = require(./userDetails.js);
  // Do something ..
}
Enter fullscreen mode Exit fullscreen mode

Here, we load a module called userDetails only if a user is present.

CommonJS loads modules synchronously, ES modules are asynchronous

One of the limitations of using require() is that it loads modules synchronously. This means that modules are loaded and processed one by one.

As you might have guessed, this can pose a few performance issues for large-scale applications that hundreds of modules. In such a case, import might outperform require() based on its asynchronous behavior.

However, the synchronous nature of require() might not be much of a problem for a small-scale application using a couple of modules.

Conclusion: CommonJS or ES modules?

For developers who still use an older version of Node.js, using the new ES module would be impractical.

Because of the sketchy support, converting an existing project to the ES modules would render the application incompatible with earlier versions of Node.js that only support CommonJS modules (that is, the require() syntax).

Thus, migrating your project to use ES modules may not be particularly beneficial.

As a beginner, it can be beneficial and convenient to learn about ES modules given that they are becoming the standard format for defining modules in JavaScript for both client side (browser) and server side (Node.js).

For new Node.js projects, ES modules provide an alternative to CommonJS. The ES modules format does offer an easier route to writing isomorphic JavaScript, which can run in either the browser or on a server.

In all, EMCAScript modules are the future of JavaScript.


200’s only ✔️ Monitor failed and slow network requests in production

Deploying a Node-based web app or website is the easy part. Making sure your Node instance continues to serve resources to your app is where things get tougher. If you’re interested in ensuring requests to the backend or third party services are successful, try LogRocket.

LogRocket Network Request Monitoring

LogRocket is like a DVR for web apps, recording literally everything that happens on your site. Instead of guessing why problems happen, you can aggregate and report on problematic network requests to quickly understand the root cause.

LogRocket instruments your app to record baseline performance timings such as page load time, time to first byte, slow network requests, and also logs Redux, NgRx, and Vuex actions/state. Start monitoring for free.

Top comments (0)