How to Convert CommonJS Modules to ESM: A Step-by-Step Guide

As JavaScript evolves, ECMAScript Modules (ESM) have become the modern standard for writing modular code. ESM supports native imports in browsers, allows for asynchronous loading, and enables better tree-shaking during bundling. If you're transitioning from CommonJS (require, module.exports), this guide shows how to update your modules and avoid common pitfalls.


Converting require to import

Named Imports

Before (CommonJS):

const { readFileSync } = require('fs');
const { myFunction1, myFunction2} = require('./myModule');

After (ESM):

import { readFileSync } from 'fs'; 

import { myFunction } from './myModule.js';
import { myFunction1, myFunction2} from './myModule.js';

Converting module.exports and exports to export

Default Exports

Before (CommonJS):

module.exports = function sayHello() {
  console.log('Hello');
};

After (ESM):

export default function sayHello() {
  console.log('Hello');
}

Named Exports

Before (CommonJS):

exports.greet = function () {
  console.log('Hi');
};
exports.farewell = function () {
  console.log('Bye');
};

After (ESM):

export function greet() {
  console.log('Hi');
}

export function farewell() {
  console.log('Bye');
}

Importing CommonJS Modules in ESM

If you're working in ESM and need to load a CommonJS module (like many older npm packages), use dynamic import():

const lodash = await import('lodash');

Note: Top-level await requires Node.js v16+ and must be in an ESM module (.mjs or with "type": "module" in your package.json).


Handling Mixed Exports (Default + Named)

Some libraries export both a default and named exports (e.g., react or chalk):

import chalk, { red, bold } from 'chalk';

If you're unsure what's being exported, check the library's documentation or inspect it:

const chalk = await import('chalk');
console.log(Object.keys(chalk)); // See what’s available

How to Enable ESM in Your Project

Option 1: Use .mjs Files

Rename your files to use the .mjs extension:

mv index.js index.mjs

Then use ESM syntax directly.


Option 2: Update package.json

Add "type": "module" to your package.json:

{
  "type": "module"
}

Now you can use .js with ESM imports/exports.

Note: Once this is set, all .js files in your project are interpreted as ESM. If you need CommonJS elsewhere (e.g., in config files), rename them to .cjs.


Combinations of CommonJS and ESM

1. module.exports = { a, b }

// CommonJS
function add(a, b) { return a + b; }
function subtract(a, b) { return a - b; }
module.exports = { add, subtract };

// ESM
export function add(a, b) { return a + b; }
export function subtract(a, b) { return a - b; }

2. module.exports = function

// CommonJS
module.exports = function greet(name) {
  return `Hello, ${name}`;
};

// ESM
export default function greet(name) {
  return `Hello, ${name}`;
}

3. exports.a = ..., exports.b = ...

// CommonJS
exports.trim = str => str.trim();
exports.upper = str => str.toUpperCase();

// ESM
export const trim = str => str.trim();
export const upper = str => str.toUpperCase();

4. module.exports = { default: fn, x: y }

// CommonJS
function core() {}
function helper() {}
module.exports = { default: core, helper };

// ESM
export default function core() {}
export function helper() {}

5. Mutated module.exports

// CommonJS
module.exports = {};
module.exports.start = () => {};
module.exports.stop = () => {};

// ESM
export function start() {}
export function stop() {}

6. Class Export

// CommonJS
class Logger {
  info(msg) { console.log(msg); }
}
module.exports = Logger;

// ESM
export default class Logger {
  info(msg) { console.log(msg); }
}

7. Named Constants + Default

// CommonJS
const BASE_URL = 'https://api.com';
function fetchData() {}
module.exports = {
  default: fetchData,
  BASE_URL,
};

// ESM
const BASE_URL = 'https://api.com';
function fetchData() {}
export default fetchData;
export { BASE_URL };

8. A Single Export for Everything


function doSomething() {
  console.log('Default function');
}

function helperOne() {
  console.log('Helper one');
}

function helperTwo() {
  console.log('Helper two');
}

// CommonJs
module.exports = {
  default: doSomething,
  helperOne,
  helperTwo
};

// ESM
export { default as doSomething, helperOne, helperTwo };

9. Hybrid Module (Works with ESM and CommonJS)

const BASE_URL = 'https://api.example.com';

function get(endpoint) {
  return fetch(`${BASE_URL}/${endpoint}`).then(res => res.json());
}

function post(endpoint, data) {
  return fetch(`${BASE_URL}/${endpoint}`, {
    method: 'POST',
    headers: { 'Content-Type': 'application/json' },
    body: JSON.stringify(data),
  }).then(res => res.json());
}

const apiClient = { get, post };

export { get, post, BASE_URL };
export default apiClient;

if (typeof module !== 'undefined') {
  module.exports = apiClient;
  module.exports.default = apiClient;
  module.exports.get = get;
  module.exports.post = post;
  module.exports.BASE_URL = BASE_URL;
}

This way, CommonJS and ESM can work smoothly; However, nowadays most folks create a UMD- Universal Module Definition using modern bundlers for their project (Webpack, Rollup, esbuild). A true UMD build contains logic to detect the current runtime environment (CommonJS, AMD, or browser globals) and adapt accordingly.

Common Pitfalls

Here are common issues developers run into when migrating:

Missing File Extensions

ESM requires full paths including file extensions.

// Incorrect
import myUtil from './util'; 

// Correct
import myUtil from './util.js';

Mixing require with import

Don’t mix module systems in the same file. Pick one — ideally ESM for new code.

Forgetting Top-Level await Limitations

Only available in ESM files. If using top-level await, ensure you're inside an ESM module or wrap the code in an async function.


I hope this can clear up any confusion for returning developers who just needed a primer to peruse or new developers trying to clear up their confusion as they navigate older code bases.

Resources

1
Subscribe to my newsletter

Read articles from K. Stephen Clark directly inside your inbox. Subscribe to the newsletter, and don't miss out.

Written by

K. Stephen Clark
K. Stephen Clark

I am a Trinbagonian front end developer with a background in public service who believes that user-centered development, teamwork, and strong communication are the ingredients for quality software and satisfied customers -- A recipe I aim to perfect. While learning new things I often save what I find useful in some repository for future use. Instead, I have opted to use this blog to share my knowledge, thoughts, ideas and experiences with others as well as a trove of information for my future self to peruse.