Post cover

How to Use ES Modules in Node.js

Updated March 22, 2023

The ECMAScript modules (in short ES modules) is a JavaScript modules format that uses import and export statements:


// An ECMAScript module
// import statement
import myFunc from './my-func';
// export statement
export myOtherFunc(param) {
const result = myFunc(param);
// ....
return otherResult;
}

Starting version 13.2.0, Node.js has stable support of ES modules.

In this post, you'll learn how to enable and use ES modules in Node.js.

1. Enabling ECMAScript modules in Node.js

The default format of modules in Node.js is the CommonJS.

But Node.js will treat a JavaScript file as an ECMAScript modules format if:

  1. The module's file extension is .mjs
  2. Or the module's nearest parent folder has { "type": "module" } in package.json
  3. Or the argument --input-type=module is present, and the module's code is passed as a string using --eval="<module-code>" argument or from STDIN.

Let's detail into the first (.mjs extension) and second ({ "type": "module" } in package.json) ways.

1.1 .mjs file extension

If you create a JavaScript file with the extension .mjs, then Node.js will consider the file an ES module.

The following ES module month-from-date.mjs (note the .mjs file extension) exports a function monthFromDate(), which determines the month name of an arbitrary date:


// month-from-date.mjs (ES Module)
const MONTHS = ['January', 'February', 'March','April', 'May', 'June',
'July', 'August', 'September', 'October', 'November', 'December'];
export function monthFromDate(date) {
if (!(date instanceof Date)) {
date = new Date(date);
}
return MONTHS[date.getMonth()];
}

Same way another module month.mjs uses the ES module import syntax to import monthFromDate() function from 'month-from-date.mjs' module. This module also runs as a CLI script, and prints the month name of the date string passed as an argument:


// month.mjs (ES Module)
import { monthFromDate } from './month-from-date.mjs';
const dateString = process.argv[2] ?? null;
console.log(monthFromDate(dateString));

That's all you need to make Node.js use ES modules!

Let's run month.mjs module in command line:


node ./month.mjs "2022-02-01"

Run the command in the demo.

February is printed in the terminal.

1.2 { "type": "module" } in package.json

By default .js files in Node.js are considered CommonJS modules. To make .js files as ES modules simply set "type" field as "module" in the package.json:


{
"name": "my-app",
"version": "1.0.0",
"type": "module",
// ...
}

Now all .js files inside the folder containing such package.json execute as ECMAScript modules.

Regarding the month modules, let's rename month-from-date.mjs to month-from-date.js and month.mjs to month.js (while still keeping the import and export syntax), set "type" field as "module" in the package.json, and Node.js is going to execute these modules as ECMAScript ones.


node ./month.js "2022-03-01"

Run the command in the demo.

March is printed in the terminal. Node.js ran successfully the ES modules month.mjs and month-from-date.mjs.

2. Importing ECMAScript modules

The specifier is the string literal having the path from where to import the module.

In the example below 'path' is a specifier:


// 'path' is the specifier
import module from 'path';

There are 3 kinds of specifiers in Node.js: relative, bare and absolute.

2.1 Relative specifier

Importing a module using a relative specifier resolves the path of the imported module relative to the current (importing) module location.

Relative specifiers usually start with '.', '..', or './':


// Relative specifiers:
import module1 from './module1.js';
import module2 from '../folder/module2.mjs';

When using relative specifiers indicating the file extension (.js, .mjs, etc.) is obligatory.

2.2 Bare specifier

A bare specifier starts with a module name (doesn't start with '.', './', '..', '/'), and imports modules from node_modules or the built-in Node.js modules.

For example, if you've installed the lodash-es package in node_modules, then you can access that module using a bare specifier:


// Bare specifiers:
import lodash from 'lodash-es';
import intersection from 'lodash-es/intersection';

Using bare specifiers you can also import the Node.js built-in modules:


import fs from 'fs';

2.3 Absolute specifier

An absolute specifier imports modules using an absolute path:


// Absolute specifier:
import module from 'file:///usr/opt/module.js';

Note the presence of the file:// prefix in the absolute specifiers.

3. Dynamic import of modules

The default importing mechanism of ES modules always evaluates the imported module: no matter if you use the module or not.

If you want to import the modules dynamically, then invoke the asynchornous function import('./path-to-module'):


async function loadModule() {
const {
default: defaultComponent,
component1
} = await import('./path-to-module');
// ...
}
loadModule();

import('./path-to-module') loads asynchronously the module. import() returns a promise that resolves to the imported module components:

  • default property as the default import
  • the named imports as properties with the same names

For example, let's improve month.js script to load month-from-date.js module only when the user sets the date argument:


// month.js (ES Module)
const dateString = process.argv[2] ?? null;
if (dateString === null) {
console.log('Please indicate date argument');
} else {
(async function() {
const { monthFromDate } = await import('./month-from-date.js');
console.log(monthFromDate(dateString));
})();
}

const { monthFromDate } = await import('./month-from-date.mjs') loads the module dynamically, and assigns the named export monthFromDate to a variable with the same name.


node ./month.js "2022-04-01"

Run the command in the demo.

April is logged in the terminal.

4. Mixing module formats

You can be in a situation when you need to import a CommonJS module from an ES module, and vice-versa.

Fortunately, Node.js allows an ES module to include a CommonJS module as a default import:


// ES module
import defaultComponent from './module.commonjs.js';
// ...

When imported in an ES module, the module.exports of the CommonJS module becomes the default import. The named imports from a CommonJS module, however, is not supported.

The require() function of the CommonJS format cannot import an ES module. Instead, you can use the async function import() inside CommonJS to load an ES module:


// CommonJS module
async function loadESModule() {
const {
default: defaultComponent,
component1
} = await import('./module.es.mjs');
// ...
}
loadESModule();

I recommend as much as possible to avoid mixing module formats because it is confusing.

5. ECMAScript modules and Node.js environment

Inside the ECMAScript module scope are not available the CommonJS specific variables like:

  • require()
  • exports
  • module.exports
  • __dirname
  • __filename

However, you can use import.meta.url to determine the absolute path of the current module:


// An ES module at path "/usr/opt/module.mjs"
console.log(import.meta.url); // "file:///usr/opt/module.mjs"

6. Conclusion

Node.js supports ES modules when the module extension is .mjs, or the nearest folder of the module has a package.json containing { β€œtype”: β€œmodule” }.

Then you can import modules using:

  • Relative path, e.g. import module from './module.js'
  • Absolute path, e.g. import module from 'file:///abs/path/module.js'
  • Modules installed in node_modules, e.g. import lodash from 'lodash-es'
  • Or built-in Node.js modules like import fs from 'fs'.

You can import dynamically a module using import('./path-to-module') syntax.

While not desirable, but sometimes necessary, you can import a CommonJS module from an ES module using the import defaultImport from './common.js' statement. The module.exports of the CommonJS becomes the default import defaultImport inside the ES module.

How to write quality ECMAScript modules? To find out, follow my post 4 Best Practices to Write Quality JavaScript Modules.

Do you think Node.js should migrate completely to ES modules format?

Like the post? Please share!

Dmitri Pavlutin

About Dmitri Pavlutin

Software developer and sometimes writer. My daily routine consists of (but not limited to) drinking coffee, coding, writing, overcoming boredom πŸ˜‰. Living in the sunny Barcelona. πŸ‡ͺπŸ‡Έ