Skip to content
Mig ration F low
infra esm commonjs modules node javascript

CommonJS ESM

Replace require/module.exports with ES module import/export syntax.

Copy for

Using an AI agent? See /agents for MCP, JSON, and llms.txt access.

CommonJS → ESM

Philosophy shift

CommonJS loads modules synchronously at runtime — require() can appear anywhere, even inside conditionals. ESM is statically analyzed: imports are hoisted and resolved before execution, enabling tree-shaking and top-level await.

Rule: ESM and CJS cannot be freely mixed. A .mjs file (or "type": "module" package) cannot require(). Plan the boundary before migrating.

Setup

Add to package.json:

{
  "type": "module"
}

All .js files in the package are treated as ESM. Rename any files that must stay CJS to .cjs.

Option B — file-by-file

Rename individual files from .js to .mjs. No package.json change needed.

TypeScript projects

// tsconfig.json
{
  "compilerOptions": {
    "module": "ESNext",
    "moduleResolution": "bundler"
  }
}

Core transformations

Imports

// CommonJS
const fs = require('fs');
const { readFile } = require('fs');
const path = require('path');
const config = require('./config.json');

// ESM
import fs from 'fs';
import { readFile } from 'fs';
import path from 'path';
import config from './config.json' assert { type: 'json' };
// Node 22+: import config from './config.json' with { type: 'json' };

Exports

// CommonJS
module.exports = { foo, bar };
module.exports = MyClass;
module.exports.helper = helper;

// ESM
export { foo, bar };
export default MyClass;
export { helper };

Default vs named exports

// CommonJS — single object export (treated as default by bundlers)
module.exports = { connect, disconnect };

// ESM — explicit named exports (preferred for tree-shaking)
export { connect, disconnect };

// or if a default shape is needed
export default { connect, disconnect };

Dynamic require → dynamic import

// CommonJS
const plugin = require(`./plugins/${name}`);

// ESM
const plugin = await import(`./plugins/${name}`);

import() returns a Promise — use it at the top level (ESM supports top-level await) or inside an async function.

__dirname and __filename

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

// ESM — no built-in equivalents; reconstruct from import.meta.url
import { fileURLToPath } from 'url';
import { dirname } from 'path';

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

require.resolve

// CommonJS
const resolved = require.resolve('./utils');

// ESM
import { createRequire } from 'module';
const require = createRequire(import.meta.url);
const resolved = require.resolve('./utils');
// or use import.meta.resolve (Node 18.19+)
const resolved = import.meta.resolve('./utils');

Conditional require

// CommonJS — lazy load inside a function
function getLogger() {
  return require('./logger');
}

// ESM — top-level import (eagerly loaded)
import logger from './logger';
// or dynamic import if truly lazy
async function getLogger() {
  return (await import('./logger')).default;
}

JSON files

// CommonJS
const pkg = require('./package.json');

// ESM (Node 22+ stable)
import pkg from './package.json' with { type: 'json' };

// Older Node — use fs.readFileSync or createRequire
import { createRequire } from 'module';
const require = createRequire(import.meta.url);
const pkg = require('./package.json');

File extension rules

CJS extensionESM extensionNotes
.js.jsWith "type": "module" in package.json
.js.mjsWithout package.json type field
.cjsstays .cjsExplicitly CJS regardless of package type
.ts.tsTypeScript with ESM target

In ESM, relative imports require the full extension:

// CJS — extension optional
const utils = require('./utils');

// ESM — extension required
import utils from './utils.js'; // even for .ts files, use .js in the import

When NOT to migrate

Pitfalls

Validation checklist

Codemod references

AI Prompt

You are migrating a Node.js file from CommonJS to ES Modules.

Rules:
1. Replace `const x = require('y')` with `import x from 'y'` or `import { x } from 'y'`.
2. Replace `module.exports = x` with `export default x` or named exports `export { x }`.
3. Replace `module.exports.x = y` with `export { y as x }`.
4. Replace `__dirname` and `__filename` with fileURLToPath/dirname reconstructed from import.meta.url.
5. Replace synchronous conditional `require()` with `await import()` inside an async function.
6. Add `.js` extension to all relative imports.
7. Do not change the runtime logic — only update module syntax.

Migrate the following file:

References