Migrate Your NodeJS App to ES Modules

Joris Verbogt
Joris Verbogt
Dec 10 2021
Posted in Engineering & Technology

A step-by-step guide

Migrate Your NodeJS App to ES Modules

Recently, Node 16 graduated to be the Long-Term Support version of NodeJS, consolidating some of the more modern features that were introduced in the previous version. But even as Node 14 adoption grew, module authors have been slowly updating their code to export as ES modules. This would be a great time for your app to do the same. Although there ways to import these modules into your good old-fashioned CommonJS code, this in general complicates your code with no added benefits.

The other way around works as-is: ESM-compliant code can still import your existing (CommonJS) dependencies.

One way or another, you will need to change your code anyway, so let's just see how to migrate to ES modules in a few easy steps.

What are these ES Modules you speak of?

Glad you asked! Let's look at a bit of history.

Ever since NodeJS came out, there has been a need to easily incorporate other pieces of code into your code. It enables developers to include code that is distributed via the Node Package Manager and it is one of the main reasons for the popularity of NodeJS as a development framework.

The standard that was popularized by NodeJS is called CommonJS. Here, modules can be exported and imported through a package metadata file package.json. CommonJS uses the require() statement to import a module or "package" into your code.

In the meantime, the JS working group decided on their own standard: the ECMAScript Modules. In short: ES modules or ESM.

ES modules use import and export statements to define what needs to be included and what is provided by a module. It is similar to CommonJS but with some added features and important differences.

An example

Let's say we have a small project that has its own piece of reusable library code and uses the popular lodash package as a dependency.

So, we have a file structure like this:

├─┬ my-library
│ └── index.js
├── index.js
└── package.json

With a package.json:

  "name": "blogpost",
  "version": "1.0.0",
  "description": "An example blog post tutorial for migration to ESM",
  "main": "index.js",
  "keywords": [
  "author": "",
  "license": "ISC",
  "dependencies": {
    "lodash": "^4.17.21"

A simple library file:

 * A sample utility class

class MyLibrary {
  constructor () {
    this.values = {}
  addValue(key, value) {
    this.values[key] = value
  getValue(key) {
    return this.values[key]

module.exports = MyLibrary

And the main app file:

 * The main file
const _ = require('lodash')
const MyLibrary = require('./my-library')
const metaData = require('./package.json')

const myLib = new MyLibrary()

myLib.addValue('test', _.sortBy(['person', 'women', 'man', 'camera', 'tv']))
myLib.addValue('version', metaData.version)

Step 1: Declare your app to be ESM compatible

There are 2 ways of doing this. The easiest and cleanest way is to migrate your whole project to use ESM. We'll take that road in this example, but if your code base is substantially big and you want to migrate in parts, it's good to know there are alternatives. I promise we'll come back to that later.

Simply declare your project type to be an ES module:

  "name": "blogpost",
  "version": "1.0.0",
  "description": "An example blog post tutorial for migration to ESM",
  "type": "module"

If you are using ESLint, you also need to declare this in your .eslintrc.json configuration:

  "env": {
    "node": true,
    "browser": true,
    "es2021": true
  "parserOptions": {
    "sourceType": "module"

Step 2: Adapt your code

Now, because all your code will be considered ESM compliant, we need to change our exports and imports.

First, let's make sure our little library correctly exports as an ES module. This is done with the export statement, which replaces the module.exports in CommonJS.

export default class MyLibrary {
  // ....

We can use several named exports, but there can be only one default, which can then be easily imported:

 * The main file
import _ from 'lodash'
import MyLibrary from './my-library/index.js'

As you can see, the import statement requires a path to a directory that contains a package.json, otherwise it needs to be the full path to a .js file.

Step 3: Refactor obsolete file imports

Now, since the import statement expects a module, it is not possible anymore to use it to import non-module files, like the package.json we use in our example.

So, for "imports" like that, we need to explicitly use fs.readFileSync and its siblings:

import { readFileSync } from 'fs'
const metaData = JSON.parse(readFileSync('./package.json', 'utf8'))

Side-step: Go by parts

As mentioned at the start of this tutorial, there is also a way to gradually migrate to ESM. Remember, if package.json declares a type "module", all .js files are considered ESM-compliant.

To force a file to be considered old-fashioned CommonJS, you can rename its file extension to .cjs. The other way around, if you use type CommonJS for .js files (so no type declaration in package.json), any file with extension .ejs will be considered ESM-compliant.

Then, after all files are migrated, you have to rename them back to .js again, so this always requires a bit more work, but you gain the option to migrate your code by parts.


We've already seen a couple of important changes that break existing code, like the loading of JSON files.

Another important change is that you can no longer use the __filename and __dirname variables. Instead, ES modules have access to import.meta.url which resolves to the current module's absolute file path, which can then be processed to obtain the filename and directory name.

Ready to take the step?

The steps above should be enough for most use-cases, but if you want more in-depth information, please take a look at the relevant parts of the NodeJS documentation.

As always, we hope you liked this article and if you have anything to add, we are available via our Support Channel.

Keep up-to-date with the latest news