A quick guide for converting your existing NodeJS projects from CommonJS to ESM/ECMAScript. Including Express, Jest, Supertest and ESLint.
ECMAScript or ESM modules are the official way of developing JavaScript software today and many projects and libraries are moving towards this format to leverage its advantages. Support for CommonJS, which was the de facto standard for NodeJS projects so far, is even being dropped by some famous libs. The question now is how your existing NodeJS projects can be migrated to the new level?
Fortunately, for the vast majority of projects this should be a no-brainer. In this guide we’ll migrate a minimal project using popular libs and tools like Express, Jest, Supertest and ESLint from CommonJS to an ESM project.
Table of Contents
tl;dr – fast-lane conversion
For the impatient here’s the straight forward uncommented list of steps to convert an existing project using Jest and ESLint from CommonJS to ESM. For more details on each step and other sidenotes see further below.
- In
package.json
: add"type": "module"
and"exports": "./start.js"
, remove the"main": "start.js"
entry. - Replace all
module.exports
statements withexport
and allrequire
statements withimport
in your source files. - Set or add
"sourceType": "module"
and"ecmaVersion": "latest"
in theparserOptions
section of your ESLint configuration. - Replace the
"jest"
start command inpackage.json
with"NODE_OPTIONS=--experimental-vm-modules npx jest"
.
That’s already it. You should now be able to run the project and all tests using ESM. Keep on reading for an example and more details on the conversion steps.
Example project on GitHub
This post is accompanied by the node-commonjs-to-esm example project on GitHub which uses Express, Jest, Supertest and ESLint. The project comes with two branches commonjs
and esm
showing the original state using CommonJS and the migrated one using ESM.
# clone the example project
git clone https://github.com/tsmx/node-commonjs-to-esm.git
# install needed dependencies
cd node-commonjs-to-esm
npm install
# check out the original CommonJS project
git checkout commonjs
# check out the migrated ESM project
git checkout esm
To start the project or test suite – regardless of the branch you are in – run the following commands.
# run the project to start a simple server on localhost:3000
# with GET routes for '/', '/route1' and '/route2'
npm run start
# run the Jest tests
npm run test
Note: When switching between the ESM and the CommonJS branch, a new npm install
is not necessary as the conversion doesn’t affect the dependencies.
Required NodeJS version
To make full use of ECMAScript/ESM and convert your projects accordingly, a NodeJS version of at least v12.20.0 or v14.13.0 is needed.
Please note that if you are on an older version of NodeJS it’s highly recommended to update anyways since these versions are quite old and even support for v14 has ended by the time of writing this article. For the example NodeJS v18 LTS was used.
CommonJS to ESM conversion steps in detail
ESM modifications to package.json
To switch from CommonJS to ECMAScript/ESM we’ll first make two slight changes to our package.json
:
# in package.json
# before
"main": "start.js",
# change to
"exports": "./start.js",
"type": "module",
For more details on exporting the entry point and the module type declaration refer to the official NodeJS documentation for package.json fields.
Replacing require/module.exports with import/export
Now the biggest change has to be done: in all of your source code files you have to replace the statements for exporting and importing as module.exports
and require
are not longer supported with ESM.
For unnamed exports do the following changes…
// before: CommonJS - unnamed export
module.exports = app;
// after: ESM - unnamed export
export default app;
And for named exports of functions…
// before: CommonJS - named exports
module.exports.route1 = function (req, res) { ... }
module.exports.route2 = (req, res) => { ... };
// after: ESM - named exports
export function route1 (req, res) { ... }
export const route2 = (req, res) => { ... };
Please note the const
keyword instead of function
when exporting an arrow function. For a complete list of available export statements for other types like arrays, classes, literals and so on please refer to the export statement documentation.
After changing all the exports, let’s move on with replacing require by import…
// before: CommonJS requiring in dependencies
const express = require('express'); // standard module
const app = require('./app'); // own module
const routes = require('./handlers/routes'); // own module with named exports routes.route1 and routes.route2
// after: ESM importing dependencies
import express from 'express'; // without trailing .js
import app from './app.js'; // with trailing .js
import * as routes from './handlers/routes.js'; // '*' to import all named exports, with trailing .js
Note that own modules must always be imported by providing the path and file extension whereas standard modules installed via npm (like express) don’t.
Importing named exports can also be done selectively if you need only some imports from a module by using destructuring in curly braces like so…
import { route1, route2 } from './handlers/routes.js';
For a full list of available import declarations please refer to the import statement documentation.
You should now already be able to start the migrated ESM project by running npm run start
.
Updating the ESLint configuration
Although everything is running, you should notice some ESLint errors in your code…
This is because ESLint isn’t aware of that you’ve switched from CommonJS to ESM. To fix that simply add the following entries at the top level to your ESLint configuration – in case of the sample project the .eslintrc.json
file.
"parserOptions": {
"ecmaVersion": "latest",
"sourceType": "module"
},
Now ESLint knows to validate against ECMAScript/ESM syntax with the latest features and no errors should show up any more. For a complete description of the available options have a look at the ESLint parsing docs.
Making Jest working again
Last thing to fix up is running the unit tests with Jest. Invoking npm run test
would give you an error like that…
To fix this, we change the starting command in our package.json
for Jest as suggested in the provided documentation site for ECMAScript modules.
# before: CommonJS Jest start script under 'scripts'
"test": "jest"
# after ESM Jest start script
"test": "NODE_OPTIONS=--experimental-vm-modules npx jest"
Although this feature is still considered being experimental, the tests are now running again.
That’s it! The project is now completely converted to ESM.
You may also check out the CommonJs vs. ESM cheat-sheet for further reading.
Happy coding 🙂