ES Modules Logo ESModules.com

Migration Guide

Convert your CommonJS codebase to ES Modules with confidence.

6-Step Migration Checklist

1

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

This makes all .js files ES Modules by default

2

Replace require() with import

Convert all CommonJS imports to ES Module syntax

3

Replace module.exports with export

Use named or default exports

4

Add .js extensions to imports

ES Modules require explicit file extensions

5

Replace __dirname and __filename

Use import.meta.url instead

6

Test thoroughly

Run your test suite and check for errors

Before & After Examples

❌ CommonJS (Before)

// utils.js
const helper = require('./helper');

function add(a, b) {
  return a + b;
}

module.exports = { add };

✅ ES Modules (After)

// utils.js
import helper from './helper.js';

export function add(a, b) {
  return a + b;
}

Common Pitfalls & Solutions

❌ Problem: Missing file extensions

import utils from './utils'; // Error!

✅ Solution: Add .js extension

import utils from './utils.js';

❌ Problem: __dirname not defined

const path = __dirname + '/file.txt'; // Error!

✅ Solution: Use import.meta.url

import { fileURLToPath } from 'url';
import { dirname } from 'path';
const __dirname = dirname(fileURLToPath(import.meta.url));

Simpler in Node.js 22+:

// Node.js 21.2+ provides these directly
const __dirname = import.meta.dirname;
const __filename = import.meta.filename;
// No imports needed!

❌ Problem: Dynamic require()

const mod = require(`./${name}.js`); // Error!

✅ Solution: Use dynamic import()

const mod = await import(`./${name}.js`);

Gradual Migration Strategy

You can migrate gradually using the dual package approach:

package.json

{
  "type": "module",
  "exports": {
    ".": {
      "import": "./dist/index.js",
      "require": "./dist/index.cjs"
    }
  }
}

This allows your package to work with both CommonJS and ES Module consumers.

The 2026 Interop Landscape

The CJS-to-ESM migration story has improved significantly:

  • require(esm) - Node.js 22+ can require() ES Modules directly (default in Node 23+), reducing the urgency of full migration
  • Deno 2.x - Full npm/Node.js backwards compatibility while being ESM-native
  • Bun - Seamless CJS/ESM interop with no configuration needed
  • TypeScript 5.8+ - --rewriteRelativeImportExtensions auto-rewrites .ts to .js in imports

Should You Still Migrate?

Yes. While interop has improved, ESM is the standard going forward. New packages increasingly ship ESM-only, and ESM enables better tree-shaking, static analysis, and tooling support.

Automated Migration Tools

Several tools can automate the mechanical parts of CJS-to-ESM conversion:

cjs-to-esm

Converts require() to import and module.exports to export. Handles most common patterns automatically.

npx cjs-to-esm ./src

jscodeshift / codemods

AST-based transform toolkit for large-scale codebase migrations. Write custom transforms for project-specific patterns.

Tip: Automated tools handle ~80% of conversions. Always review the output and test thoroughly - edge cases like conditional require() and dynamic paths need manual attention.