Esm Js Meaning: Migrating from CommonJS to ESM in Node.js

Published on December 16, 2025 | M.E.A.N Stack Development
WhatsApp Us

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() with import and module.exports with export.
  • Key Challenge: __dirname is not available; use import.meta.url with the url and path modules.
  • 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/export syntax 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.

  1. 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).

  2. Enable ESM in Your Package.json

    Add "type": "module" to your project's root package.json file. This tells Node.js to treat all .js files 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 .mjs extension for ESM files and keep your existing files as .js (CommonJS).

  3. Update Your File Extensions (If Necessary)

    If you are using the "type": "module" approach, you can keep your .js extensions. If you are using the incremental .mjs approach, rename your ES module files accordingly. Any file that needs to remain CommonJS (e.g., a legacy config file) should be renamed to .cjs.

  4. 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 };
  5. Handle Node.js Globals: __dirname and __filename

    This is a major stumbling block. In ESM, __dirname and __filename are not defined. You must reconstruct them using the import.meta.url meta-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.

  6. Update Dynamic Imports

    In CommonJS, you might have used require() dynamically. In ESM, you must use the import() 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 export

    Because import() is asynchronous, you'll often need to use it inside async functions.

  7. 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/globals for Jest).

  8. 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.json has "type": "module".
  • You are not running the file with an older Node.js version (< 13.2.0).
  • You are not accidentally using a .cjs file 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:recommended and appropriate parser options to catch import/export errors early.
  • Leverage TypeScript: If you use TypeScript, set "module": "NodeNext" or "ESNext" in your tsconfig.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

Is ESM faster than CommonJS in Node.js?
Currently, the performance difference is often negligible and can vary by use case. The primary benefits of ESM are not raw speed but improved developer experience, tooling integration, and ecosystem alignment. Future Node.js optimizations are more likely to target ESM.
Can I use both require() and import in the same file?
No. A file is either an ES module or a CommonJS module. You cannot use the syntax of both in the same source file. However, an ES module file can dynamically 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).
My tests broke after migration. What should I do?
Testing frameworks need explicit ESM support. For Jest, ensure you are using a recent version and configure 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.