The Complete Guide to Migrating from CommonJS to ESM in Node.js
Looking for esm js meaning training? Quick Answer: Migrating from CommonJS to ESM (ECMAScript Modules) in Node.js involves changing your project's package.json to use "type": "module", updating all your .js file extensions to .mjs (or using the .js extension with the package.json setting), and systematically replacing CommonJS syntax like require() and module.exports with ESM's import and export. The main challenges are handling Node.js-specific globals like __dirname and adapting to ESM's stricter, asynchronous nature.
- Core Change: Set
"type": "module"in your package.json. - Syntax Swap: Replace
require()withimportandmodule.exportswithexport. - Key Challenge:
__dirnameis not available; useimport.meta.urlwith theurlandpathmodules. - Dynamic Imports: Use the
import()function, which returns a Promise. - Benefit: Unlocks modern JavaScript features, better tooling, and a unified module system across front-end and back-end.
For years, Node.js developers have lived comfortably in the world of CommonJS, using require()
and module.exports to structure their applications. However, the JavaScript ecosystem has
evolved, and the official ECMAScript module standard (ESM) is now the future. If you're working on a modern
Node.js project, migrating to ESM isn't just a trendy upgrade—it's a strategic move for long-term
maintainability, performance, and compatibility with the broader JS ecosystem. This guide will walk you
through the why, the how, and the crucial gotchas of making this transition, with practical solutions for
real-world problems like handling __dirname and dynamic imports.
What is ESM in Node.js?
ECMAScript Modules (ESM) is the official, standardized module system for JavaScript, defined by the
ECMAScript specification. Unlike CommonJS, which was Node.js's original, server-focused solution, ESM is
designed to work universally across browsers, servers, and other JavaScript environments. In Node.js, ESM
support became stable in version 13.2.0 and is now the recommended module system for new projects. It uses
the import and export keywords, supports static analysis for better tooling and
tree-shaking, and executes in strict mode by default.
CommonJS vs ES Modules: A Detailed Comparison
Understanding the fundamental differences between these two systems is key to a successful migration. The shift is more than just syntax; it's a change in philosophy and behavior.
| Criteria | CommonJS | ES Modules (ESM) |
|---|---|---|
| Syntax | const fs = require('fs'); module.exports = fs; |
import fs from 'fs'; export default fs; |
| Loading | Synchronous. Modules are loaded at runtime. | Asynchronous (in browsers) and can be statically analyzed. In Node.js, the top-level import is
synchronous, but dynamic import() is async. |
| File Extension | Uses .js (or .cjs if needed). |
Uses .mjs or .js with "type": "module" in package.json. |
Top-Level this |
Refers to module.exports. |
Is undefined (strict mode is mandatory). |
__dirname & __filename |
Globally available. | Not available. Must be derived from import.meta.url. |
| Interoperability | Can import ESM only via dynamic import(). |
Can import CommonJS modules, but with some caveats (default exports are wrapped). |
| Primary Use Case | Legacy Node.js projects, npm packages that haven't migrated. | Modern Node.js projects, full-stack JavaScript, and libraries aiming for universal compatibility. |
Why Migrate to ESM in Modern Node.js?
The decision to migrate should be driven by tangible benefits for your project's health and your team's productivity.
- Unified Language: Use the same
import/exportsyntax on both the front-end (React, Vue, Angular) and back-end. This reduces cognitive load and makes code sharing easier. - Static Analysis & Tree-Shaking: Bundlers like Webpack and Vite can analyze ESM imports at build time to eliminate unused code (tree-shaking), resulting in smaller production bundles.
- Native Browser Support: ESM runs natively in modern browsers, simplifying isomorphic JavaScript and server-side rendering setups.
- Future-Proofing: The JavaScript ecosystem is rapidly standardizing on ESM. New tools, libraries, and best practices are being built with ESM as the default assumption.
- Performance Potential: While not always faster in Node.js currently, the asynchronous nature and static structure of ESM open doors for future optimizations.
If you're building skills for a modern web development career, understanding ESM is non-negotiable. Our Node.js Mastery course dives deep into these modern paradigms with hands-on projects, ensuring you learn the theory and the practical implementation.
Step-by-Step Migration Guide
Follow this systematic process to migrate your Node.js project from CommonJS to ESM. It's best to do this in a dedicated branch and have a good test suite in place.
-
Audit Your Dependencies
Before changing anything, check if your key dependencies support ESM. Look for
"exports"fields in their package.json or check their documentation. Most modern libraries now provide dual support (CommonJS and ESM). -
Enable ESM in Your Package.json
Add
"type": "module"to your project's rootpackage.jsonfile. This tells Node.js to treat all.jsfiles as ES modules.// package.json { "name": "my-app", "version": "1.0.0", "type": "module", // <-- This is the key line "scripts": { ... }, "dependencies": { ... } }Alternative: If you want to migrate incrementally, you can use the
.mjsextension for ESM files and keep your existing files as.js(CommonJS). -
Update Your File Extensions (If Necessary)
If you are using the
"type": "module"approach, you can keep your.jsextensions. If you are using the incremental.mjsapproach, rename your ES module files accordingly. Any file that needs to remain CommonJS (e.g., a legacy config file) should be renamed to.cjs. -
Convert Require Statements to Imports
This is the bulk of the work. Go file by file and replace CommonJS syntax.
// CommonJS const express = require('express'); const { readFile } = require('fs').promises; module.exports = myFunction; // ESM Equivalent import express from 'express'; import { readFile } from 'fs/promises'; export default myFunction; // For named exports // CommonJS: exports.myFunction = myFunction; // ESM: export { myFunction }; -
Handle Node.js Globals: __dirname and __filename
This is a major stumbling block. In ESM,
__dirnameand__filenameare not defined. You must reconstruct them using theimport.meta.urlmeta-property.import { fileURLToPath } from 'url'; import { dirname } from 'path'; const __filename = fileURLToPath(import.meta.url); const __dirname = dirname(__filename); // Now you can use __dirname as before console.log(__dirname);Consider creating a utility file (
esm-utils.js) that exports these reconstructed variables to avoid repeating this code everywhere. -
Update Dynamic Imports
In CommonJS, you might have used
require()dynamically. In ESM, you must use theimport()function, which returns a Promise.// CommonJS Dynamic Import const modulePath = './config-' + env + '.js'; const config = require(modulePath); // ESM Dynamic Import const modulePath = './config-' + env + '.js'; const configModule = await import(modulePath); const config = configModule.default; // Access the default exportBecause
import()is asynchronous, you'll often need to use it insideasyncfunctions. -
Update Your Tooling Configuration
Your testing framework (Jest, Mocha), linter (ESLint), and other tools need to be configured for ESM. This often involves setting specific flags or installing plugins (e.g.,
@jest/globalsfor Jest). -
Test Thoroughly
Run your entire test suite. Pay special attention to module loading paths, JSON imports (which now require an assertion:
import data from './data.json' assert { type: 'json' };), and any conditional logic around module loading.
Pro Tip from Practical Experience: Don't try to migrate a large, monolithic project in one go. Start with a new, isolated service or a small utility module within your project. This low-risk approach builds confidence and familiarizes your team with the new patterns before tackling the core application. For a guided, project-based approach to mastering such architectural decisions, explore our Full-Stack Development program.
Common Challenges and Their Solutions
1. JSON Imports
CommonJS allowed require('./data.json'). ESM requires an experimental flag or a special
assertion (currently experimental). The most common stable workaround is to use the fs module
to read the file.
import { readFile } from 'fs/promises';
const jsonData = JSON.parse(await readFile(new URL('./data.json', import.meta.url)));
2. Interoperating with CommonJS Packages
You can import CommonJS npm packages seamlessly. However, note that a CommonJS module's
module.exports is treated as the ESM default export. If you need named imports,
ensure the CommonJS package uses the "exports" field correctly in its package.json.
3. The "Cannot use import statement outside a module" Error
This is the most common error. It means Node.js is treating your file as CommonJS. Double-check:
- Your
package.jsonhas"type": "module". - You are not running the file with an older Node.js version (< 13.2.0).
- You are not accidentally using a
.cjsfile or a file in a subdirectory with its own package.json that overrides the type.
Best Practices for a Smooth Migration
- Use a Linter: Configure ESLint with the
eslint:recommendedand appropriate parser options to catch import/export errors early. - Leverage TypeScript: If you use TypeScript, set
"module": "NodeNext"or"ESNext"in yourtsconfig.json. TypeScript's compiler will help enforce correct ESM syntax. - Create ESM-Specific Scripts: Add a
"start:esm"script to your package.json to run your ESM build, making it easy for all developers to test. - Document the Patterns: Create a small internal wiki page documenting how your team
handles
__dirname, JSON imports, and dynamic imports in the new ESM codebase.
Seeing these concepts in action can solidify your understanding. For visual learners, we break down module systems, bundling, and modern Node.js architecture in our LeadWithSkills YouTube channel, where we provide tutorials that bridge the gap between theory and practice.
FAQs on Migrating to Node.js ESM
import() a
CommonJS module, and a CommonJS module can use require() on an ES module only if it uses
the dynamic import() keyword (which returns a Promise).transform: {} or use --experimental-vm-modules.
For Mocha, use the --loader flag or the newer experimental specifiers
Ready to Master Node.js?
Transform your career with our comprehensive Node.js & Full Stack courses. Learn from industry experts with live 1:1 mentorship.