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
requirefunction. - Export: Using
module.exportsorexports.
// 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
importstatement. - Export: Using
exportorexport 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:
- File Extensions: Use
.mjsto force ES Modules, or.cjsto force CommonJS. - package.json configuration: Set the
typefield to"module"in your configuration file to treat all.jsfiles 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);
}