ES modules in Node today
Tags: node, javascript
Published: 2020-03-18 13:55:41

---
title: ES modules in Node today
published: true
date: 2020-03-03 17:00:21 UTC
tags: node,javascript
canonical_url: https://blog.logrocket.com/es-modules-in-node-today/
cover_image: https://dev-to-uploads.s3.amazonaws.com/i/j1qdm4r37kd10klsi649.jpeg
---
**Written by [Alexander Nnakwue](https://blog.logrocket.com/author/alexandernnakwue/)**✏️

## Introduction

> “Before software can be reusable, it first has to be usable.” – [Ralph Johnson](https://en.wikipedia.org/wiki/Ralph_Johnson_(computer_scientist))

[Modules](https://en.wikipedia.org/wiki/Module) are independent building blocks of a software program. They are basically a design pattern that implements features of [m](https://en.wikipedia.org/wiki/Modular_programming)[odular design](https://en.wikipedia.org/wiki/Modular_programming) in programming languages. The module system is supported in many languages and is quite popular since the way dependencies are handled, packaged, and managed determines how easy it is to work with a large and growing source code.

In modular design, business logic pertaining to a particular feature or functionality is packaged (modularized) in a standardized format for reusability, flexibility, and for the sake of reducing complexity. This setup affords a loosely coupled system due to a smooth interface of communication, as there are no global variables or shared state.

Although the concept of modules is [quite different depending on the language](https://en.wikipedia.org/wiki/Modular_programming#Terminology), they are akin to the idea of [namespaces](https://en.wikipedia.org/wiki/Module_pattern#Namespaces) in languages like Java. Modules enable code organization by splitting a codebase into reusable components such that each performs individual functions and can be combined or composed to form larger functionalities or an entire application.

In Node.js, the [module system](https://nodejs.org/docs/latest/api/modules.html#modules_modules) has come a long way from its earlier adoption of [CommonJS](https://requirejs.org/docs/commonjs.html). Today, [ECMAScript modules](https://nodejs.org/dist/latest-v13.x/docs/api/esm.html#esm_ecmascript_modules) (ES modules), though still [experimental](https://nodejs.org/api/documentation.html#documentation_stability_index) at the time of writing, are the official standard for packaging code for reuse in both client- and server-side JavaScript.

[![LogRocket Free Trial Banner](https://i0.wp.com/blog.logrocket.com/wp-content/uploads/2017/03/f760c-1gpjapknnuyhu8esa3z0jga.png?resize=1200%2C280&ssl=1)](https://logrocket.com/signup/)

## Table of contents

In this article, we are going to learn about ES modules in Node. However, we will briefly explore other ways of handling and organizing server-side code with CommonJS.

Why? So that we have a point of reference to recognize the benefits of ES modules. In essence, we will learn about the challenges it tries to solve that earlier module systems were not adapted to solve.

We will be looking at:

- **An introduction to ES modules** — here, we introduce ES modules in an exciting way
- **A brief history of ES modules** — here, we learn about the transition from the earlier module system to ES modules. We will also briefly examine how interoperable these modules systems are with one another
- **Adding support for ES modules in Node** — here, we learn about how we can incrementally add support for ES modules in Node. We also learn how to migrate an old codebase to start using ES modules
- **Comparing and contrasting features** — here, we will learn about the features of both these module systems and how they compare
- **ES modules moving forward**

## Prerequisites

To easily follow along with this tutorial, it is advisable to have the latest version of Node.js installed. Instructions on how to do so are available in the [Node documentation](https://nodejs.org/en/).

Also, for better context, readers may need to be fairly knowledgeable with the [CommonJS](https://requirejs.org/docs/commonjs.html) module system in Node. It is equally welcoming for newcomers learning the Node.js module system or applying ES modules in their Node projects today.

## Introducing ES modules

With the release of [Node version 13.9.0](https://github.com/nodejs/node/blob/master/doc/changelogs/CHANGELOG_V13.md#2020-02-18-version-1390-current-codebytere), [ES modules](https://nodejs.org/api/esm.html#esm_introduction) can now be used without an experimental flag since they are enabled by default. With ES modules, modules are defined with the use of the `import` and `export` keywords instead of the `require()` function in CommonJS. Here is how they are used:

```jsx
export function sayLanguage(language) {
    console.log(`I love ${language}!`);
  }

//f.js


import {sayLanguage} from './f.js';

console.log(sayLanguage('JavaScript'));

//g.js


retina@alex es-modules in Node % node -v
v13.7.0
retina@alex es-modules in Node % node g.js 
(node:77133) ExperimentalWarning: The ESM module loader is experimental.
I love JavaScript!
undefined
retina@alex es-modules in Node %
```

Details about these keyword bindings can be found in the spec [here](https://tc39.es/ecma262/#prod-ModuleItem). Also, we can peek at the Mozilla development network [doc](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Statements) for more information.

> **Note** : The code snippet above shows the `ExperimentalWarning` for the module loader due to the Node version on my local machine. However, it is likely this would change in later Node versions.

## Adding support for ES modules in Node today

This support was previously behind the `--experimental-module` flag. Though this is no longer required, the implementation remains experimental and subject to change.

Files ending with `.mjs` or `.js` extensions (with the nearest `package.json` file with a field `type`) are treated as ES modules, as shown on `line 9` below:

```jsx
{
  "name": "es_modules_in_node",
  "version": "1.0.0",
  "description": "A sample for enabling ES Modules in Node.js",
  "main": "g.js",
  "scripts": {
    "test": "echo \"Error: no test specified\" && exit 1"
  },
  "type": "module",
  "keywords": [
    "ES",
    "MODULES",
    "NODE",
    "MODULES",
    "JS"
  ],
  "author": "Alexander Nnakwue",
  "license": "MIT"
}
```

So, in essence, when we run `node g.js` in the same folder as the above `package.json`, the file is treated as an ESM. Additionally, it is an ESM if we are passing string arguments to the Node.js standard input with flag `--input-type=module`.

> **Note** : Without the `type` field in the parent `package.json` file, Node throws the error shown below:

```jsx
(node:2844) Warning: To load an ES module, set "type": "module" in the package.json or use the .mjs extension.
```

## Package scope

A package scope, defined by the `type` flag in a parent `package.json` file and all folders below it, is present in the current scope of that package, as explained earlier. Furthermore, files ending with the `.mjs` extension are always loaded as ES modules regardless of the scope of that package.

In the same light, all other forms of files without extensions and without the `type` flag in the parent `package.json` file are treated as CommonJS. Additionally, files ending with `.cjs` extensions are treated as CJS modules regardless of package scope.

## Import and export syntax

In ES modules, specifiers are like string-based file paths used after the `from` keyword. There are both [algorithms](https://nodejs.org/api/esm.html#esm_resolution_algorithm) to load an ES module specifier and to determine the module format of a resolved URL. An example is shown below:

```jsx
import {cat} from 'animals';
```

The `animal` specifier in this case is an npm package, for example. Other ways specifiers can be referenced include from both absolute and relative file paths or URLs, and paths within other packages. Examples are shown in this [section](https://nodejs.org/api/esm.html#esm_import_specifiers) of the documentation.

Although `import` statements are permitted only in ES modules, they can reference both an ESM or CommonJS modules. For example:

```jsx
import packageMain from 'commonjs-package'; // Works

import { method } from 'commonjs-package'; // Errors
```

> **Note** : [Import statements](https://nodejs.org/api/esm.html#esm_import_statements) that reference other types of files like JSON or Wasm are still experimental and require the `--experimental-json-modules` and `--experimental-wasm-modules` flags, respectively, to be passed. Details can be found [here](https://nodejs.org/api/esm.html#esm_experimental_json_modules).

For exports in ES modules, we can make use of the following:

- Named exports
```jsx
module.exports.name = "Alex"
```
- Default exports
```jsx
export default function sayName() {console.log('My name is Mat')}
```

> **Note** : All built-in Node packages support all types of exports (named, default) described above.

## Package entry points

There are now two fields that can define entry points for a package: `main` and `exports`. More details can be found in the [documentation](https://nodejs.org/api/esm.html#esm_package_entry_points).

> **Note** : Despite the current support for ES modules in Node, there are still some known concerns with interoperability with CommonJS. We will explore this further in the section on interoperability.

## CommonJS module system

Prior to the introduction of ES modules, the community relied heavily on CommonJS for packaging server-side JavaScript code. In the CommonJS module system, each file is treated as a module, which exposes a set of APIs (via a well-defined interface) with the use of the [`exports` object](https://github.com/nodejs/ecmascript-modules/blob/modules-lkgr/doc/api/modules.md#exports). To understand this better, here is an example using the object created by the module system:

```jsx
function sayName(name) {
    console.log(`My name is ${name}.`)
  };

function sayAge(age){
  console.log(`I'm ${age} years old.`)
  };


module.exports = {sayName, sayAge};
//a.js
```

To use these functions (imported as modules in a different file), we can use the [`require` function](https://github.com/nodejs/ecmascript-modules/blob/modules-lkgr/doc/api/modules.md#requireid). This accepts a module identifier (ID) specified by either a relative or an absolute path or by name, based on the module type of the exposed APIs, like so:

```jsx
const {sayName, sayAge} = require('./a') 
// assuming a.js is in the same folder path

console.log(sayName('Alex')) // My name is Alex.

console.log(sayAge(25)) // I'm 25 years old.

//b.js
//TO RUN THE CODE SAMPLE TYPE: $ node b.js on your terminal
```

As we can see above, the `require` object returns the module content exported from the `a.js` file. To learn more about the [implementation](https://github.com/nodejs/node/blob/master/lib/internal/modules/cjs/loader.js#L187) of the `module`, `export`, and `require` keywords, we can peek at the module wrapper [here](https://github.com/nodejs/ecmascript-modules/blob/modules-lkgr/doc/api/modules.md#the-module-wrapper).

The CommonJS specification is also available [here](https://github.com/commonjs/commonjs/blob/master/docs/specs/modules/1.0.html.markdown). The spec [highlights](https://github.com/commonjs/commonjs/blob/master/docs/specs/modules/1.0.html.markdown#commonjs-modules) the minimum features that a module system must have in order to support and be interoperable with other module systems.

The CommonJS implementation allows for a defined structure in how files are loaded. In this approach, code required from other files are loaded or parsed synchronously. For this reason, catching and detecting failure points or debugging code are easier and less tedious.

Why? Because variables present in the modules or exported files are within the scope of that module or private to it and not in the global scope, as such errors are properly propagated. Also, because of the huge separation of concern, modules are loaded from parent to child, traversing down the dependency graph.

> **Note** : When the wrapper function returns, the exports object is cached, then returned as the return value for the `require()` method. This is like a cycle. How? On the next call to the `module.exports` method, the same process of wrapping the module with the wrapper function is repeated. But not without checking if that module is already stored in the cache.

The signature of the wrapper function is shown below:

```jsx
(function(exports, require, module, __filename, __dirname) {
// Module code actually lives in here
});
```

The `Module` object, which takes in an ID and a parent module as parameters, contains the `export` object:

```jsx
function Module(id = '', parent) {
  this.id = id;
  this.path = path.dirname(id);
  this.exports = {};
  this.parent = parent;
  updateChildren(parent, this, false);
  this.filename = null;
  this.loaded = false;
  this.children = [];
};
```

The `updateChildren` method scans through the file path until the root of the file system is reached. Its job is to update the `children` property of the `Module` object with the new `parent`, as the case may be. Here is the signature below:

```jsx
function updateChildren(parent, child, scan) {
  const children = parent && parent.children;
  if (children && !(scan && children.includes(child)))
   children.push(child);
}
```

Let’s see an example to understand this better. In the `b.js` file above, add this line of code to print the module and the argument object:

```jsx
console.log(module, arguments);
```

After running `node b.js`, we get the following output:

```jsx
retina@alex es-modules in Node % node b.js
My name is Alex.
undefined
I'm 25 years old.
undefined
<ref *1> Module {
  id: '.',
  path: '/Users/retina/Desktop/es-modules in Node',
  exports: {},
  parent: null,
  filename: '/Users/retina/Desktop/es-modules in Node/b.js',
  loaded: false,
  children: [
    Module {
      id: '/Users/retina/Desktop/es-modules in Node/a.js',
      path: '/Users/retina/Desktop/es-modules in Node',
      exports: [Object],
      parent: [Circular *1],
      filename: '/Users/retina/Desktop/es-modules in Node/a.js',
      loaded: true,
      children: [],
      paths: [Array]
    }
  ],
  paths: [
    '/Users/retina/Desktop/es-modules in Node/node_modules',
    '/Users/retina/Desktop/node_modules',
    '/Users/retina/node_modules',
    '/Users/node_modules',
    '/node_modules'
  ]
} [Arguments] {
  '0': {},
  '1': [Function: require] {
    resolve: [Function: resolve] { paths: [Function: paths] },
    main: Module {
      id: '.',
      path: '/Users/retina/Desktop/es-modules in Node',
      exports: {},
      parent: null,
      filename: '/Users/retina/Desktop/es-modules in Node/b.js',
      loaded: false,
      children: [Array],
      paths: [Array]
    },
    extensions: [Object: null prototype] {
      '.js': [Function (anonymous)],
      '.json': [Function (anonymous)],
      '.node': [Function (anonymous)]
    },
    cache: [Object: null prototype] {
      '/Users/retina/Desktop/es-modules in Node/b.js': [Module],
      '/Users/retina/Desktop/es-modules in Node/a.js': [Module]
    }
  },
  '2': Module {
    id: '.',
    path: '/Users/retina/Desktop/es-modules in Node',
    exports: {},
    parent: null,
    filename: '/Users/retina/Desktop/es-modules in Node/b.js',
    loaded: false,
    children: [ [Module] ],
    paths: [
      '/Users/retina/Desktop/es-modules in Node/node_modules',
      '/Users/retina/Desktop/node_modules',
      '/Users/retina/node_modules',
      '/Users/node_modules',
      '/node_modules'
    ]
  },
  '3': '/Users/retina/Desktop/es-modules in Node/b.js',
  '4': '/Users/retina/Desktop/es-modules in Node'
}
```

As shown above, we can see the [module object](https://github.com/nodejs/node/blob/master/lib/internal/modules/cjs/loader.js#L156) on line 6 with all the properties, including the `filename`, `id`, `children`, path depth, etc. Also, we can see the `argument` object, which consists of the `export` object, `require` function, file and folder path, and the `Module` (which is essentially what the wrapper function does, but it executes the code contained in a file/module).

Finally, as an exercise, we can go ahead and print the `require` function in the `b.js` file. To learn more about the output of the `require` function, we can check the [implementation](https://github.com/nodejs/node/blob/master/lib/internal/modules/cjs/loader.js#L1087) in this section of the Node source code.

> **Note** : Special emphasis should be placed on the `load` and `validateString` methods. The `validateString` method, as its name implies, checks whether the passed-in module ID is a valid string. To understand the require function on a much higher level, you can check the [Knowledge section](https://nodejs.org/en/knowledge/getting-started/what-is-require/) of the Node documentation.

## Interoperability for both module systems

In CommonJS, [modules are wrapped as functions](https://github.com/nodejs/ecmascript-modules/blob/modules-lkgr/doc/api/modules.md#the-module-wrapper) before they are evaluated at runtime. For ES modules, code reuse provided via `import` and `export` [binding](https://github.com/nodejs/node-eps/blob/master/002-es-modules.md#21-types) are already [created or loaded](https://github.com/nodejs/node-eps/blob/master/002-es-modules.md#31-async-loading) asynchronously before they are evaluated. To understand how ESM works under the hood, you can check [here](https://github.com/nodejs/node-eps/blob/master/002-es-modules.md#22-operations). Now let’s explore further 🙂

For a quick comparison, a CommonJS module goes through this phase in its lifecycle:

_Resolution –> Loading –> Wrapping –> Evaluation –> Caching_

This validates the fact that for CommonJS, there is no way to determine what gets exported as a module until the module is wrapped and evaluated. This is quite different for ES modules, as the imported symbols are already [parsed and understood by the language](https://github.com/nodejs/node-eps/blob/master/002-es-modules.md#22-operations) before the code gets evaluated.

When the code is parsed, just before it is evaluated, an internal [Module Record](https://tc39.es/ecma262/#sec-source-text-module-records) is created, and only after this data structure is well-formed are the files parsed and the code evaluated.

For example:

```jsx
//d.mjs
const check = () => {
  console.log('Just checking`);
};
export.check = check;


//e.mjs assuming they are on the same folder path
import {check} from './d'
```

In the `e.mjs` file above, Node.js parses and validates the imports before going further to execute or evaluate the piece of code. This is not the case for a CommonJS module: the exported symbols are only made known after the module is wrapped and evaluated.

This incompatibility is one of the many reasons the standard body in charge of ECMAScript intended to implement interoperability for both ESM and Node’s existing CommonJS module system.

Furthermore, the [current specifier resolution](https://nodejs.org/api/esm.html#esm_customizing_esm_specifier_resolution_algorithm) does not support all default behavior of the CommonJS loader. One of the major differences is automatic resolution of file extensions and the ability to import directories that have an index file.

For example, if we do an `import './directory'` from, say, a directory that has an `index.js`, ES modules does not look for an `index.js` file in the specified folder, as was the case in CommonJS. Instead, it throws an error. This can be fixed by passing the experimental flag `--experimental-specifier-resolution=[mode]`.

> **Note** : To customize the default module resolution, loader hooks can optionally be provided via the `--experimental-loader ./loader-name.mjs` argument to Node.js. The loaders APIs are currently being redesigned, which means they would change in the future.

More details about interoperability with CommonJS can be found in this [section](https://nodejs.org/api/esm.html#esm_interoperability_with_commonjs) of the documentation.

## Features of both module systems

- Dynamic `import()` is supported in both CommonJS and ES modules. It can be used to include ES module files from CommonJS code.
- ECMAScript 6 also provides that modules can be loaded from a URL, while CommonJS is limited to relative and absolute file paths. [This new improvement](https://nodejs.org/api/esm.html#esm_https_loader) not only makes loading more complicated, but also slow.
- Sources that are in formats Node.js doesn’t understand can be converted into JavaScript. More details can be found [here](https://nodejs.org/api/esm.html#esm_transpiler_loader).
- Support for [extensionless main entry points](https://github.com/WICG/import-maps#extension-less-imports) in ESM has been dropped.
- In the current release of the standard, loading internal modules from disk has been enabled, as specified in the [changelog](https://github.com/nodejs/node/pull/31321).
- [proposal-import-meta](https://github.com/tc39/proposal-import-meta) provides the absolute URL of the current ES module file. It is currently a stage 3 proposal in the TC39 spec.
- The dynamic imports proposal, currently in stage 4 of the TC39 draft, can be used to import both ES and CommonJS modules. Note that this statement returns a promise.
- A file extension must be provided when using the `import` keyword. Directory indexes (e.g., `'./database/index.js'`) must be fully specified.
- Dual CommonJS and ESM are now possible with the use of [conditional exports.](https://nodejs.org/api/esm.html#esm_conditional_exports) Now, Node.js can run ES module entry points, and a package can contain both CommonJS and ESM entry points.

> **Note** : Function `require` shouldn’t be used in ES modules.

## ES modules moving forward

ES modules is still tagged experimental since the feature is not fully ready for production environments. This is because there is a need to remove current changes that lack support for backwards compatibility.

The challenge, therefore, is upon package authors, maintainers, and developers to be explicit with package naming and other useful conventions. More details about this can be found [here](https://nodejs.org/api/esm.html#esm_writing_dual_packages_while_avoiding_or_minimizing_hazards).

> “The main goal is to preserve backward compatibility moving forward, now that Node.js supports both CommonJS and ES modules.”

Nowadays, it is possible to use both CommonJS and ESM in one application, but there are still some issues associated with it. CommonJS modules need to know if the module being loaded is a CommonJS or an ES module since the latter is loaded only asynchronously.

Also, in accordance with the ESM spec, using the `import` keyword does not complete the file path by default with the file name extension, as for CommonJS modules. Therefore, this must be explicitly stated.

## Conclusion and next steps

Prior to the introduction of the ES6 standard, there wasn’t any native implementation for organizing source code in server-side JavaScript. The community relied heavily on CommonJS module format.

Nowadays, with the introduction of ES modules, developers can enjoy the many benefits associated with the release specification. This article has highlighted the transition between both module systems and their interoperability.

Finally, due to the existing incompatibility issues between both module formats, the current transition from CommonJS to ESM would be quite a challenge due to the issues we have highlighted in this article. Tools like [Babel](https://babeljs.io/) and [esm](https://github.com/standard-things/esm#readme), which translate the newer syntax into code compatible with older environments, can make the transition easier.

In the long run, this entire draft process is an important step and paves the way for further future improvements. Let me know if you have any questions in the comment section below, or message me on my [Twitter handle](https://twitter.com/alex_nnakwue). Thanks for reading 🙂

* * *
## 200's only ‎✅: Monitor failed and show GraphQL requests in production 

While GraphQL has some features for debugging requests and responses, making sure GraphQL reliably serves resources to your production app is where things get tougher. If you’re interested in ensuring network requests to the backend or third party services are successful, [try LogRocket.](https://www2.logrocket.com/signup-lr)

![Alt Text](https://thepracticaldev.s3.amazonaws.com/i/jd2amkhuiynfjf3i33qk.png)

[LogRocket](https://www2.logrocket.com/signup-lr) 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 GraphQL requests to quickly understand the root cause. In addition, you can track Apollo client state and inspect GraphQL queries' key-value pairs.

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.](https://www2.logrocket.com/signup-lr)
* * *

The post [ES modules in Node today](https://blog.logrocket.com/es-modules-in-node-today/) appeared first on [LogRocket Blog](https://blog.logrocket.com).