Brian De Sousa Geek. Dad. Husband. Developer. Traveler.

ES modules in Node.js 12, from experimental to release

5 min read 1578

ES Modules In Node.js 12

Different forms of modularization have been around in the JavaScript ecosystem for years. Developers have used well-defined specifications such as AMD or CommonJS as well as simple coding patterns like the revealing module pattern to gain the benefits of a well-modularized solution.

Modules can be used on the client side in browsers or on the server side in Node.js. Sometimes code is transpiled from one module format to another using tools like Babel. All of this makes for a messy and complex JavaScript module state.

Enter ES modules — more specifically, ES modules in Node.js.

Tip: This article focuses on ES modules in Node.js. Check out “CommonJS vs AMD vs RequireJS vs ES6 Modules” for an excellent comparison of module systems not specific to Node.js.

A brief history of ES module support

Let’s look at some of the key milestones for ES module support:

  • June 2015 – September 2017: Major browsers add experimental support for ES modules hidden behind developer flags. The primary means of developing JavaScript using ES modules is by transpiling code using tools like Babel.
  • September 2017: Node.js v8.5 includes experimental support for ES modules.
  • September 2017 – May 2018: Major browsers begin to support the ES module specification without developer flags, including:
    1. Chrome 61, on 5 September 2017
    2. Safari 11, on 18 September 2017
    3. Firefox 60, on 8 May 2018
  • October 2018: A new module implementation plan is created. The plan includes several phases for replacing the current experimental implementation with a new implementation, following three guiding principles from day one:
    1. Comply with the ES specification
    2. Node should do things the same way browsers do as much as possible
    3. Don’t break existing CommonJS modules

Tip: The Node.js Modules team has provided a more detailed set of guiding principles for the new implementation.

  • October 2019: Node 12 entered long-term support. The goal was to release full support for ES modules.

What is ES module support and why is it so important for Node.js?

For a couple reasons. For one thing, all major browsers already support ES modules — you can see for yourself here. Supporting ES modules on the server side in Node.js out of the box will allow full-stack developers to naturally write modular, reusable JavaScript for both the client and server.

For another thing, experimental features in Node.js are subject to non-backward-compatible changes or removal in future versions. That being said, experimental ES module support has been around in Node for a few years and is not expected to change dramatically before October 2019.

Modules in Node.js

What are CommonJS modules?

The de facto standard for modules in Node.js currently (mid-2019 at the time of writing) is CommonJS. CommonJS modules are defined in normal .js files using module.exports. Modules can be used later within other .js files with the require() function. For example:

// foo.js
module.exports = function() { 
  return 'Hello foo!';

// index.js
var foo = require('./foo');
console.log(foo()); // Hello foo!

Use Node to run this example with node index.js.

ES modules

Since Node v8.5, developers have been able to run variations of support for the ES modules specification using the --experimental-modules flag. As of Node v12.4, modules can be defined in .mjs files (or .js files under certain circumstances). For example:

// foo.mjs
export function foo() { 
  return 'Hello foo!'; 

// index.mjs
import { foo } from './foo.mjs';
console.log(foo()); // Hello foo!

Use Node to run this example with node --experimental-modules index.mjs.

Using CommonJS and ES modules in the same application

In some ways, supporting ES modules in browsers may have been a little simpler than supporting ES modules in Node because Node already had a well-defined CommonJS module system. Luckily, the community has done a fantastic job of ensuring developers can work with both types of modules at the same time and even import from one to the other.

For example, let’s say we have two modules. The first is a CommonJS module, and the second is an ES module (note the different file extensions):

// cjs-module-a.js
module.exports = function() {
  return 'I am CJS module A';

// esm-module-a.mjs
export function esmModuleA() {
  return 'I am ESM Module A';
export default esmModuleA;

To use the CommonJS module in an ES module script (note the .mjs extension and use of the import keyword):

// index.mjs
import esmModuleA from './esm-module-a.mjs';
import cjsModuleA from './cjs-module-a.js';
console.log(`esmModuleA loaded from an ES Module: ${esmModuleA()}`);
console.log(`cjsModuleA loaded from an ES Module: ${cjsModuleA()}`);

Use Node to run this example with node --experimental-modules index.mjs.

To use the ES module in a standard CommonJS script (note the .js extension and use of the require() function):

// index.js
// synchronously load CommonJS module
const cjsModuleA = require('./cjs-module-a');
console.log(`cjsModuleA loaded synchronously from an CJS Module: ${cjsModuleA()}`);

// asynchronously load ES module using CommonJS
async function main() {
  const {esmModuleA} = await import('./esm-module-a.mjs');
  console.log(`esmModuleA loaded asynchronously from a CJS module: ${esmModuleA()}`);

These examples provide a basic demonstration of how to use CommonJS and ES modules together in the same application. Check out “Native ES Modules in NodeJS: Status and Future Directions, Part I” by Gil Tayar for a deeper dive into CommonJS and ES Module interoperability.

Modules in Node.js: Future state

At the time of writing, the new module implementation plan is in its third and final phase. Phase 3 is planned to be completed at the same time that Node 12 LTS is released and when ES module support will be available without the -experimental-modules flag.

Phase 3 will likely bring a few big improvements to round out the ES module implementation.

Loaders solution

Developers expect module loading systems to be flexible and full-featured. Here are a few of the key features in development in the Node.js module loader solution:

  • Code coverage/instrumentation: Enable developer tools to retrieve data about CJS and ESM module usage.
  • Pluggable loaders: Allow developers to include loader plugins in their packages that can define new behaviors for loading modules from specific file extensions or mimetypes, or even files without extensions.
  • Runtime loaders: Allow files referenced in import statements to be transpiled at import time (runtime).
  • Arbitrary sources for modules: Allow modules to be loaded from sources other than the file system (e.g., load a module from a URL).
  • Mock modules: Allow modules to be replaced with mocks while testing.

You can view the full list here.

"exports" object in package.json

While the naming and syntax is not final, the idea here is to have an object somewhere in the package.json file that allows packages to provide “pretty” entry points for different components within the package. Take this package.json as an example of a possible implementation:

  "name": "@myorg/mypackage",
  "version": "1.0.0",
  "type": "module",
  "main": "./dist/index.js",
  "exports": {
    ".": "./src/mypackage.mjs",
    "./data": "./data/somedir/someotherdir/index.mjs"

Developers would be able to import the data component of @myorg/mypackage like this:

import { MyModule } from '@myorg/mypackage/data

Referencing the package root with the package’s name

When referencing one module from another module within the same package, you may end up with a lot of backtracking that looks like this:

import coolcomponent from '../../../coolcomponent/module.js

If this change is implemented, then backtracking can be replaced with a reference to the package’s name as defined in package.json. The new code would look like this:

import coolcomponent from 'mypackage/coolcomponent/module.js

Supporting dual ESM/CommonJS packages

Allowing an npm package to contain CJS and ES modules alongside each other is important to ensure there is a backwards-compatible, developer-friendly path to migrate from CommonJS to ES modules. This has often been referred to as “dual-mode” support.

The status quo approach to dual-mode support is for the existing main entry point in package.json to point to a CommonJS entry point. If an npm package contains ES modules and the developer wants to use them, they need to use deep imports to access those modules (e.g., import 'pkg/module.mjs'). This is the dual-mode solution that is likely to ship with Node.js 12 LTS.

There were some other proposals for dual-mode support. This widely debated proposal includes some options for making it easier for developers to ship packages with two separate implementations (ESM and CJS), but this proposal failed to reach consensus.

A newer proposal for require of ESM suggests a different approach that allows developers to resolve ES modules with require(). This proposal is still open but has went silent and is unlikely to be included in Node 12 LTS.

ES modules vs. CommonJS

While the goal is for ES modules to eventually replace CommonJS modules in Node.js, no one knows what the future holds — nor how long before CommonJS module support disappears. But one thing is for sure: Node developers have spent considerable time and effort ensuring a seamless transition to a future without CommonJS.

They have done a fantastic job striking a balance between ensuring both module types interoperate with each other while trying not to introduce too many new dual-mode APIs that would become useless once the critical mass has migrated and it comes time to remove support for CommonJS from Node.

So when will CommonJS be removed from Node.js? Let’s make a wild, baseless prediction and say Node 18 with an --experimental-no-commonjs-modules and Node 20 for the final sunset. The future of modular JavaScript across browsers, servers and everywhere else JavaScript runs is an exciting one!

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 is like a DVR for web and mobile apps, recording literally everything that happens while a user interacts with your app. 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. .
Brian De Sousa Geek. Dad. Husband. Developer. Traveler.

5 Replies to “ES modules in Node.js 12, from experimental to release”

  1. Thank you for clarification! And one question is still not covered: why .mjs? Does it mean we should have both identical. js and mjs files in a package for browsers/babel and node?

  2. From what I understand, NodeJS developers considered several options for being able to suppor ES modules alongside existing CommonJS modules including not using the .mjs extension. At the end of the day, I believe the decision was made to use the .mjs extension because it was a simply way to identify an ES module over plain JavaScript or CommonJS modules. It also sort of fits in the JavaScript community with other frameworks and tools using file extensions as meta-data (ex. typescript uses .ts, react has .jsx, etc.). Here’s a great write up on other options that were considered:

    Browsers will be able to load modules from .mjs files as well but the .mjs extension will not identify that a script contains an ES module. In the browser, you’ll need to include the type=”module” attribute on your script tags, for example:


Leave a Reply