Back to roadmaps nodejs Course

CommonJS vs ES Modules

Node.js supports two distinct module systems: CommonJS (CJS), the legacy system, and ES Modules (ESM), the modern JavaScript standard. Understanding their differences is crucial for project setup and package management.


1. Syntax Differences

The most obvious difference lies in how dependencies are imported and exported.

CommonJS (CJS)

  • Import: Using the require function.
  • Export: Using module.exports or exports.
// mathUtils.js (CommonJS Export)
const add = (a, b) => a + b;
const subtract = (a, b) => a - b;

module.exports = {
  add,
  subtract
};

// index.js (CommonJS Import)
const { add, subtract } = require("./mathUtils");
console.log(add(5, 3));

ES Modules (ESM)

  • Import: Using the import statement.
  • Export: Using export or export default.
// mathUtils.mjs (ESM Export)
export const add = (a, b) => a + b;
export const subtract = (a, b) => a - b;

// index.mjs (ESM Import)
import { add, subtract } from "./mathUtils.mjs";
console.log(add(5, 3));

2. Under the Hood: Loading Mechanisms

CJS and ESM behave very differently during runtime module resolution.

Feature CommonJS (CJS) ES Modules (ESM)
Loading Mode Synchronous Asynchronous
Resolution Time Runtime Compile-time (Static Analysis)
File Extension .js or .cjs .js (with type: module) or .mjs
Top-Level Await Not supported Supported
Special Variables __dirname, __filename available Not available (use import.meta.url)

Static Analysis vs Dynamic Evaluation

CommonJS imports are evaluated dynamically at runtime. This means you can wrap a require statement inside conditional branches:

// Valid in CommonJS
if (process.env.NODE_ENV === "development") {
  const devLogger = require("./devLogger");
}

ES Modules are statically analyzed before code execution. Imports must reside at the top level, allowing bundlers to perform optimization tasks like Tree Shaking (eliminating unused code).


3. How Node.js Selects the Module System

By default, Node.js treats files ending in .js as CommonJS. You can control this behavior in two ways:

  1. File Extensions: Use .mjs to force ES Modules, or .cjs to force CommonJS.
  2. package.json configuration: Set the type field to "module" in your configuration file to treat all .js files as ES Modules.

Example package.json:

{
  "name": "modern-node-app",
  "type": "module"
}

4. Replicating CJS Features in ESM

In ES Modules, global variables like __dirname and __filename do not exist. You must reconstruct them using the metadata object:

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

// Get filename and directory path in ESM
const __filename = fileURLToPath(import.meta.url);
const __dirname = dirname(__filename);

console.log("Current Directory:", __dirname);
console.log("Current File:", __filename);

5. Dynamic Imports in ES Modules

Although ESM imports are static, you can load modules dynamically at runtime using the dynamic import() function. This returns a promise:

// Dynamic import works in both CJS and ESM environments
async function loadConfig() {
  const config = await import("./config.json", {
    with: { type: "json" }
  });
  console.log("Loaded configurations:", config.default);
}
Published on Last updated: