Skip to content
Theme:

All you need to know to move from CommonJS to ECMAScript Modules (ESM) in Node.js

One of the most revolutionary features introduced as part of ECMAScript 2015 specification is modules (ESM). The first browser implementation landed in April 2017 in Safari 10.1. I published a “Native ECMAScript modules in the browser” about this historical moment. A few months later, in September 2017, Node v8.5.0 landed with experimental support for ESM.

This feature went through lots of iterations during its experimental phase. A few years later, in April 2020, Node v14.0.0 landed without experimental modules warning. Even though it was still experimental, it felt like the right timing to adopt ESM for some toy projects and insignificant clients work. Eventually, Node v15.3.0 arrived and marked modules implementation as stable.

ECMAScript modules marked as stable in version 15.3

That’s enough of history, so let’s get our hands dirty and dive into the ECMAScript modules in Node.js. We have a lot to cover, so let’s jump into it!

Enabling ECMAScript modules (ESM) in Node.js

To preserve backward compatibility, Node.js treats JavaScript code as CommonJS by default. To enable ESM, we have three options.

  • use .mjs extension (colloquially known as Michel’s Jackson’s modules)
  • add "type": "module" to package.json file
  • use --input-type=module flag for STDIN or strings passed to --eval argument

Syntax

ECMAScript module introduced a new syntax. Have a look at the example written in CommonJS and equivalent using ESM.

// util.js
module.exports.logger = (msg) => console.log(`👌 ${msg}`);

// index.js
const { logger } = require("./util");

logger("CommonJS");
// 👌 CommonJS
// util.js
const logger = (msg) => console.log(`👌 ${msg}`);

export { logger };

// index.js
import { logger } from "./util.js";

logger("ECMAScript modules");
// 👌 ECMAScript modules

There’s a lot more to explore in terms of syntax but I will leave that with you as the Node.js closely conforms to official ESCMAScript modules syntax. Pay attention to the file extension (.js or .mjs) needed to correctly resolve relative or absolute specifiers. This rule also applies to directory indexes compared to CommonJS (e.g. ./routes/index.js).

Strict by default

There is no need for use strict on the top of your program to prevent the runtime from running in sloppy mode. ECMAScript modules run in strict mode by default.

Browser compatibility

Because ESM implementation in Node.js and the browser conforms to the exact specification, we can share code between server and client runtime. In my opinion, the unified syntax is one of the most appealing benefits of using ESM.

<srcipt type="module" src="./index.js"> </srcipt>

“Get Ready For ESM” by Sindre Sorhus goes in-depth about other benefits of unified syntax and encourages package creators to make a move to ESM. I can’t agree more with this fantastic dude!

ESM is missing some references

ECMAScript modules enabled runtime is missing some commonly used in CommonJS references:

  • exports
  • module
  • __filename
  • __dirname
  • require
console.log(exports);
// ReferenceError: exports is not defined

console.log(module);
// ReferenceError: module is not defined

console.log(__filename);
// ReferenceError: __filename is not defined

console.log(__dirname);
// ReferenceError: __dirname is not defined

console.log(require);
// ReferenceError: require is not defined

As we discussed above, when using ESM, we don’t need access to exports and module anymore. We can recreate the remaining references that are missing.

// Recreate missing reference to __filename and __dirname
import { fileURLToPath } from "url";
import { dirname } from "path";

const __filename = fileURLToPath(import.meta.url);
const __dirname = dirname(__filename);

console.log(__dirname);
console.log(__filename);

Node v21.2.0 added a very helpful shortcuts for this, so this pollyfill is no longer needed. The import.meta.dirname and import.meta.filename are much nicer options.

// Recreate missing reference to require
import { createRequire } from "module";

const require = createRequire(import.meta.url);

Behavior of this keyword

It’s worth mentioning that the behaviour of this keyword differs in the global scope. In ESM, this is undefined, however in CommonJS, this keyword points to exports. Worth remembering this subtle difference.

// this keyword in ESM
console.log(this);
// undefined
// this keyword in CommonJS
console.log(this === module.exports);
// true

From dynamically parsed CommonJS to statically parsed ESM

CommonJS modules are parsed dynamically during the execution phase. This functionality allows calling the require function inside the block (i.e. inside if statement) as the dependency graph is explored during the program execution.

ECMAScript modules are much more sophisticated — before running, the interpreter will build a dependency graph and then execute the program. Predefined dependencies graph allows the engine to perform optimizations such a tree shaking (dead code elimination) and more.

ESM with top-level await support

Node.js in version 14 enabled support for top-level await. This changes dependency graph rules a little and makes a module act like a big async function. Example time!

import { promises as fs } from "fs";

// Look ma, no async function wrapper!
console.log(JSON.parse(await fs.readFile("./package.json")).type);
// module

Importing JSON

Importing JSON is a frequently used feature in CommonJS. Unfortunately, doing that while using ESM will throw an error. As recommended above, we can overcome this limitation by recreating require function.

import data from "./data.json";
// TypeError [ERR_UNKNOWN_FILE_EXTENSION]: Unknown file extension ".json"
import { createRequire } from "module";
const require = createRequire(import.meta.url);
const data = require("./data.json");

console.log(data);
// {"that": "works"}

The best time to embrace ESM is now

I hope this article helped you to understand the differences between CommonJS and ECMAScript modules in Node.js. I’m looking forward to the times where we won’t have to care about these differences anymore. The whole ecosystem will work accordingly to the ECMAScript specification regardless of the runtime (either client or server). If you haven’t already, I would highly recommend you jump on the ESM camp now and contribute to the consistent and unified JavaScript ecosystem.

I enjoyed writing this down for you! If you found it helpful, “hit that like button and don’t forget to subscribe…”. Nah, I’m just joking. Share it with your friend — this will mean the world to me! Thanks, and until next time, stay curious 👋

Comments

  • D
    Dan Overlander

    Do you have any recommendations regarding client-side implementation? We specify a type: "module" in our package.json, which is why commonJS is not available, and I'm trying to create a local devserver by running webpack serve, but require is unavailable.

    While trying to recreate that reference, I receive: "Failed to resolve module specifier "module". Relative references must start with..." So, I expect this must not be available client-side?

    👆 you can use Markdown here

    Your comment is awaiting moderation. Thanks!
    • Pawel Grzybek
      Pawel Grzybek

      To help you out with your issue, I will need more context. I need more than the information provided to help you out. Though I can shorten your pain when fighting with webpack, I can highly recommend Vite for front-end applications. It comes with support for ESM out of the box.

      👆 you can use Markdown here

      Your comment is awaiting moderation. Thanks!

Leave a comment

👆 you can use Markdown here

Your comment is awaiting moderation. Thanks!