CommonJS vs ES Modules: The Node.js Divide
Node.js was born in 2009, years before JavaScript had an official module system. To solve the problem of organizing code, Node.js adopted **CommonJS** (CJS). For over a decade, `require()` and `module.exports` were the standard. However, the introduction of **ES Modules** (ESM) in ECMAScript 2015 (ES6) created a schism in the ecosystem that developers must now navigate.
1. Loading: Synchronous vs. Asynchronous
The most fundamental difference lies in how modules are loaded.
- CommonJS is synchronous. When Node encounters `require('./file')`, it halts execution, reads the file from the disk, runs it, and returns the exports before moving to the next line. This is acceptable on servers where files are local.
- ES Modules are asynchronous. The `import` statements are parsed statically before code execution. This allows for fetching modules over a network (crucial for browsers) and enables **Top-Level Await** in Node.js, allowing you to await promises at the root of your module.
2. Strict Mode and The "Global" Scope
ES Modules are always executed in **Strict Mode** (`"use strict"`). You cannot disable it. This implies certain safeguards, like variables not accidentally leaking to the global scope.
CommonJS Globals
You have access to magic variables:
console.log(__dirname);
console.log(__filename);ESM Equivalents
`__dirname` does not exist. You must replicate it:
import { fileURLToPath } from 'url';
const __dirname = fileURLToPath(import.meta.url);3. Interoperability: Mixing Oil and Water
Node.js goes to great lengths to make them work together, but there are rules:
- ESM can import CJS: You can use `import _ from 'lodash'` (a CJS package) inside an `.mjs` file. However, you can only import the *default export* (the `module.exports` object). Named imports (e.g., `import { map } from 'lodash'`) might not work unless the CJS package is specially configured.
- CJS cannot `require` ESM: Because ESM is async, CommonJS cannot synchronously load it. If you need to load an ESM package in CJS, you must use the dynamic `import()` function: `const data = await import('./file.mjs')`.
Recommendation: For new Node.js projects, default to ES Modules by adding"type": "module"to yourpackage.json. It is the standard for the entire JavaScript ecosystem (browsers, bundlers, Deno, Bun).