Back to Blog
NodeJS

CommonJS vs ES Modules: A Deep Dive into Node.js Module Systems

9/29/2025
5 min read
CommonJS vs ES Modules: A Deep Dive into Node.js Module Systems

Struggling with require and import? This in-depth guide explains CommonJS and ES Modules in Node.js, their differences, interoperability, and best practices to modernize your code.

CommonJS vs ES Modules: A Deep Dive into Node.js Module Systems

CommonJS vs ES Modules: A Deep Dive into Node.js Module Systems

CommonJS vs ES Modules: The Complete Guide to Node.js Modules in 2024

If you've written any Node.js code, you've used modules. They are the fundamental building blocks that let you split your code into manageable, reusable pieces. But if you've been around for a while, or even if you're just starting, you've likely encountered two different ways to do this: the trusty old require() and the modern import statement.

This divide between CommonJS (CJS) and ECMAScript Modules (ESM) is one of the most common sources of confusion for Node.js developers. It can lead to frustrating errors, confusing documentation, and a lot of head-scratching.

So, what's the deal? Why are there two systems? Which one should you use? And can they ever get along?

In this comprehensive guide, we're going to demystify it all. We'll travel back in time to understand why CommonJS was created, then see how ES Modules emerged as a standard, and finally, learn how to navigate the modern Node.js landscape where both coexist. We'll cover definitions, examples, real-world use cases, best practices, and answer your frequently asked questions.

To learn professional software development courses such as Python Programming, Full Stack Development, and MERN Stack, visit and enroll today at codercrafter.in.


Part 1: The Origins - How We Got Here

To understand the present, we have to look at the past. The story of modules in JavaScript is a story of necessity, community, and eventual standardization.

What is CommonJS? The Server-Side Savior

In the early days of JavaScript, it was a language purely for the browser. There was no native way to include code from one file into another. Everything lived in the global scope, leading to "namespace pollution" and unmaintainable code. Then, Node.js came along in 2009, bringing JavaScript to the server.

But a server-side language needs a robust way to manage dependencies and scope. This is where the CommonJS project came in. It was a community-driven effort to standardize module systems for JavaScript outside the browser. Node.js adopted its module specification, and thus, CommonJS modules were born.

Key Characteristics of CommonJS:

  • Synchronous Loading: Modules are loaded and executed at the moment they are require()-d. This is perfectly fine on a server where file I/O is fast and happens locally.

  • Dynamic: The require() function is a regular function call. You can use it anywhere in your code—inside conditionals, loops, or functions. This makes it highly dynamic.

    javascript

    // This is valid CommonJS
    if (user.isAdmin) {
      const adminModule = require('./adminModule');
      adminModule.doSecretTask();
    }
  • Runtime Resolution: The module graph (which module depends on which) is built while your program is running.

CommonJS served Node.js incredibly well for over a decade. It's simple, intuitive, and powerful. The vast majority of packages on npm (the Node Package Manager) are still written in CommonJS.

What are ES Modules (ESM)? The Standard Arrives

While CommonJS was thriving on the server, the web community was also struggling with modules. Solutions like AMD (Asynchronous Module Definition) and UMD (Universal Module Definition) emerged, but it was clear that JavaScript needed a native, standard module system.

Enter ECMAScript Modules (ESM). This was a module system defined as part of the ECMAScript language specification itself (starting with ES6 in 2015). It was designed to work in both browsers and Node.js, creating a universal standard.

Key Characteristics of ESM:

  • Asynchronous Loading: Modules can be loaded asynchronously, which is a huge performance benefit in browsers where network latency is a factor. In Node.js, this also enables better optimizations.

  • Static: The import and export syntax must be at the top-level of your file (with an exception for dynamic imports). This allows for static analysis—tools and the JavaScript engine itself can understand the module structure without running the code.

  • Strict Mode by Default: ESM code always runs in strict mode.

  • Top-Level Await: You can use the await keyword at the top level of a module, which is not allowed in CommonJS scripts.

  • Compile-Time Resolution: The module graph can be determined before the code is executed, enabling features like "tree-shaking" (dead code elimination) in bundlers like Webpack and Vite.

The introduction of ESM was a landmark moment, but it created a challenge for Node.js: how to integrate this new standard without breaking the entire existing ecosystem?


Part 2: The Nitty-Gritty - Syntax and Behavior Compared

Let's get our hands dirty and look at the concrete differences in how you write and use these two systems.

CommonJS in Action

Exporting:

You can export a single value or an object containing multiple values.

javascript

// module.js

// Export a single function
module.exports = function add(a, b) {
  return a + b;
};

// Or, export an object with multiple values
module.exports = {
  add: (a, b) => a + b,
  subtract: (a, b) => a - b,
  PI: 3.14159
};

// Alternatively, you can assign properties to `exports` directly
exports.add = (a, b) => a + b;
exports.subtract = (a, b) => a - b;

Importing:

You use the require() function, which returns the value of module.exports from the target module.

javascript

// app.js
const add = require('./module.js'); // If a single function was exported
const result = add(5, 3);

// Or, if an object was exported
const mathOps = require('./module.js');
const result = mathOps.add(5, 3);
console.log(mathOps.PI);

ES Modules (ESM) in Action

Exporting:

You use the export keyword.

javascript

// module.mjs
// Note the .mjs extension, or "type": "module" in package.json

// Named Exports (multiple per module)
export function add(a, b) {
  return a + b;
}
export function subtract(a, b) {
  return a - b;
}
export const PI = 3.14159;

// Or, declare everything and export at the end
function multiply(a, b) { return a * b; }
function divide(a, b) { return a / b; }
export { multiply, divide };

// Default Export (one per module)
export default function greet(name) {
  return `Hello, ${name}!`;
}

Importing:

You use the import keyword.

javascript

// app.mjs
// Import named exports - they must be destructured
import { add, subtract, PI } from './module.mjs';
console.log(add(5, 3));
console.log(PI);

// Import named exports with an alias
import { add as additionFunction } from './module.mjs';

// Import the default export
import greet from './module.mjs';
console.log(greet('Alice'));

// Import everything as a namespace object
import * as mathOps from './module.mjs';
console.log(mathOps.add(10, 2));

Key Behavioral Differences

This is where the rubber meets the road. The syntax is different, but the underlying behavior is what really matters.

  1. Synchronous vs. Asynchronous:

    • require() is synchronous. It blocks the event loop until the entire module is loaded, executed, and its exports are ready.

    • import is asynchronous. The module loading process can happen in the background, and the engine can optimize the order of execution.

  2. Dynamic vs. Static:

    • CommonJS is dynamic. require('./path/' + variable) is perfectly valid. This is powerful for things like feature flags or loading modules based on user input.

    • ESM is static (for the most part). import ... from ... statements must have a string literal. This allows for advanced static analysis and optimizations. However, ESM offers a dynamic import() function for cases where you need runtime flexibility.

      javascript

      // ESM Dynamic Import (returns a Promise)
      if (needsMath) {
        const mathModule = await import('./module.mjs');
        mathModule.add(2, 2);
      }
  3. The module.exports vs export Object:

    • In CommonJS, module.exports is a single object that gets passed around. When you require it, you get that object.

    • In ESM, export creates a live, read-only view of the bound values. This is a subtle but important difference.

      javascript

      // ESM Example - Live Binding
      // counter.mjs
      export let count = 0;
      export function increment() { count++; }
      
      // app.mjs
      import { count, increment } from './counter.mjs';
      console.log(count); // 0
      increment();
      console.log(count); // 1 <-- The imported 'count' reflects the change!

      In CommonJS, you would get a copied value, not a live binding.

  4. Top-Level await:

    • This is a feature exclusive to ESM. You can use await outside of an async function at the top level of your module.

      javascript

      // ESM only!
      const data = await fetchSomeDataFromAnAPI();
      console.log(data);

      This is incredibly useful for loading configuration or data at startup. In CommonJS, you would have to wrap this logic in an async function.


Part 3: The Real-World Guide - Using Them Today

So, how do you actually tell Node.js which system you're using? And how do they interact?

Enabling ESM in Your Node.js Project

Node.js supports ESM, but you need to signal your intent. Here are the primary ways:

  1. Use the .mjs Extension: Name your files with the .mjs extension. Node.js will treat them as ESM.

  2. Use the .cjs Extension: Name your files with the .cjs extension. Node.js will treat them as CommonJS, even if you have other settings.

  3. Set "type": "module" in package.json: This is the most common approach for new projects. It tells Node.js that all .js files in your project should be treated as ESM.

    json

    {
      "name": "my-esm-app",
      "version": "1.0.0",
      "type": "module", // <-- This is the key line
      "main": "index.js"
    }
  4. The --input-type Flag: You can pass --input-type=module when running a string of code via the CLI.

Interoperability: Making CJS and ESM Work Together

This is the crucial part for a gradual migration or when using older npm packages.

Importing CommonJS from an ESM Context

This is straightforward. You can import a CommonJS module as if it were an ESM module with a default export.

javascript

// commonjs-module.cjs
module.exports = {
  hello: 'world',
  doThing: () => console.log('Thing done!')
};

// esm-app.mjs
import cjsModule from './commonjs-module.cjs'; // Works!
console.log(cjsModule.hello); // 'world'

Node.js does a good job of making this feel natural. It essentially wraps the CommonJS module to make it ESM-compatible.

Importing ESM from a CommonJS Context

This is trickier, and often not recommended. You cannot use require() to load an ESM module.

javascript

// This will FAIL!
const esmModule = require('./esm-module.mjs');

The only way to load an ESM module from a CommonJS file is by using the dynamic import() function. Remember, this returns a Promise.

javascript

// commonjs-app.cjs
async function main() {
  const esmModule = await import('./esm-module.mjs');
  console.log(esmModule.someExportedValue);
}
main();

This works, but it forces your CommonJS code to become asynchronous, which can be a significant architectural change.

Real-World Use Cases and Migration Strategy

  • Greenfield Project (New Project): Use ES Modules. Set "type": "module" in your package.json and use the modern import/export syntax. You are future-proofing your application.

  • Large, Existing CommonJS Codebase: Don't panic and rewrite everything. The best strategy is a gradual one.

    1. Start by converting your package.json to "type": "module".

    2. Rename your existing files that are meant to stay as CommonJS from .js to .cjs.

    3. Now, you can start writing all new files as ESM (using .js or .mjs). You can also gradually convert old .cjs files to .js (ESM) one by one.

  • Library Author: This is a critical one. The current best practice is to provide dual publishing.

    • Publish your package as both CommonJS and ESM.

    • In your package.json, use the "exports" field to specify different entry points for require and import.

    json

    {
      "name": "my-awesome-library",
      "exports": {
        "require": "./dist/index.cjs",
        "import": "./dist/index.mjs"
      }
    }

    This gives the consumer's bundler or Node.js runtime the optimal format.

Understanding these patterns is a core skill for a professional developer. To master such architectural decisions and modern full-stack development, explore the comprehensive courses at codercrafter.in.


Part 4: Best Practices and FAQs

Best Practices for 2024 and Beyond

  1. Prefer ESM for New Projects: It's the standard, it's the future, and it enables better tooling and performance optimizations.

  2. Be Cautious with Interoperability: While importing CJS from ESM is easy, the reverse is not. Design your project boundaries accordingly.

  3. Use .cjs and .mjs Extensions for Clarity: In mixed projects, using explicit extensions removes all ambiguity for both developers and Node.js.

  4. Leverage the "exports" Field in package.json: This modern field provides a cleaner, more secure way to define your package's public API compared to "main".

  5. Use a Bundler for Complex Applications: For front-end code or complex apps, tools like Vite, Webpack, or Rollup will handle module interop for you and provide advanced features like tree-shaking.

Frequently Asked Questions (FAQs)

Q1: Which one is faster, CommonJS or ESM?
A: In practice, for most applications, the difference is negligible. However, ESM has a theoretical performance advantage due to its asynchronous nature and static analyzability, which allows for optimizations like tree-shaking. For server-side code, the bottleneck is rarely the module system itself.

Q2: I'm getting a "Cannot use import statement outside a module" error. Help!
A: This is the most common error. It means Node.js is trying to parse an ESM file as CommonJS. To fix it, ensure you are using one of the methods to enable ESM: add "type": "module" to your package.json, or rename your file to .mjs.

Q3: What about __dirname and __filename? They don't work in ESM!
A: Correct. These CommonJS globals are not available in ESM. Instead, you can use the import.meta.url meta-property and the url and path modules to reconstruct them.

javascript

import { fileURLToPath } from 'url';
import { dirname } from 'path';

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

Q4: Should I convert my old projects to ESM?
A: If it ain't broke, don't fix it. The effort to convert a large, stable CommonJS project to ESM is often not worth the benefit. Focus on using ESM for all new development.

Q5: Can I use them both in the same file?
A: No. A single file is either a CommonJS module or an ES Module. It cannot be both.


Conclusion: Embracing the Module Future

The journey from CommonJS to ES Modules is a story of the JavaScript ecosystem maturing. CommonJS was a brilliant, pragmatic solution that allowed Node.js to thrive. ES Modules are the standardized, universal, and forward-looking evolution.

To summarize:

  • CommonJS: Dynamic, synchronous, and the bedrock of the Node.js ecosystem. Use require/module.exports.

  • ES Modules: Static, asynchronous, and the official standard for JavaScript. Use import/export.

For any new project you start today, you should be using ES Modules. They are no longer "the new thing"—they are "the thing." The tooling, the ecosystem, and the language itself are all aligned in this direction. For existing projects, migrate gradually and pragmatically.

The ability to navigate this landscape confidently is a mark of a seasoned developer. It's about understanding the history, the technical trade-offs, and the practical paths forward.

We hope this deep dive has cleared up the confusion and given you the knowledge to use both module systems effectively.


Ready to build the next generation of web applications? Solidify your understanding of Node.js, advanced JavaScript, and full-stack architectures with our project-based courses. To learn professional software development courses such as Python Programming, Full Stack Development, and MERN Stack, visit and enroll today at codercrafter.in.

Related Articles

Call UsWhatsApp
CommonJS vs ES Modules: A Deep Dive into Node.js Module Systems | CoderCrafter