
Node.js started life with a CommonJS module system, which was straightforward: require() calls and module.exports. This model was synchronous, designed for local files, and optimized for server-side JavaScript where the file system was easily accessible.
However, the JavaScript ecosystem adopted ES modules (ESM) as a standard, bringing import and export syntax that browsers use natively. This created friction because Node.js needed to support both systems simultaneously without breaking existing codebases.
The main difference lies in how modules are loaded and resolved. CommonJS modules are loaded synchronously, allowing dynamic requires anywhere in the code. ES modules, in contrast, are statically analyzable and loaded asynchronously, requiring the import statements to be at the top level.
Node.js tackled this by introducing a dual module system where files ending in .mjs or packages specifying "type": "module" in their package.json are treated as ES modules. Files ending in .cjs or by default are treated as CommonJS. This lets you gradually migrate or interoperate.
One tricky aspect is interop. ES modules import CommonJS using a default export, whereas CommonJS require() calls return the entire module.exports object. This often requires boilerplate or helpers like import() dynamic calls or the createRequire function from module to import ES modules into CommonJS.
Here’s a simple example showing how to import a CommonJS module in an ES module context:
import { createRequire } from 'module';
const require = createRequire(import.meta.url);
const commonjsModule = require('./commonjs-module.cjs');
Conversely, importing ES modules from CommonJS requires dynamic import:
(async () => {
const esModule = await import('./esm-module.mjs');
console.log(esModule.default);
})();
The evolution didn’t come without trade-offs. The Node.js loader had to be rewritten to handle both resolution algorithms, and subtle bugs arise around cyclic dependencies and default exports. But this dual system is the practical compromise to support legacy code and modern JavaScript standards.
Understanding these nuances very important to writing modular Node.js applications today. It’s not just about syntax, but about how your code is resolved and executed. The choice between static and dynamic loading impacts performance, tooling, and debugging.
In the next step, you’ll configure your project to explicitly activate ES modules, making sure Node.js interprets your files the way you intend. This involves tweaking your package.json and file extensions to signal the runtime how to treat your source files, which is a fundamental step before fully embracing the ESM style.
Getting that configuration right avoids runtime errors like ERR_REQUIRE_ESM or confusing module resolution conflicts. It’s a small upfront cost for the clarity and future-proofing of your codebase. But before diving into configuration, you must internalize the distinction between module types and the rationale behind Node’s dual approach – it’s the foundation for everything else.
Elebase USB to USB C Adapter 4 Pack,Type C Female to A Male Charger Converter for Apple Watch Ultra iWatch 8 7,14 13 12 11 Pro Plus Max,Airpods,iPad 9 10 Air 5 Mini 6,Samsung Galaxy S23 S22 S21
$8.99 (as of June 3, 2026 23:09 GMT +00:00 - More infoProduct prices and availability are accurate as of the date/time indicated and are subject to change. Any price and availability information displayed on [relevant Amazon Site(s), as applicable] at the time of purchase will apply to the purchase of this product.)Configuring package settings to activate ES modules
To activate ES modules in your Node.js project, the primary mechanism is to declare "type": "module" inside your package.json. This tells Node.js to treat all .js files within that package as ES modules by default, rather than CommonJS.
Here’s an example package.json snippet:
{
"name": "my-esm-project",
"version": "1.0.0",
"type": "module",
"main": "index.js",
"scripts": {
"start": "node index.js"
}
}
With "type": "module" set, you can write your source files using ES module syntax without changing the file extension to .mjs. This simplifies the project structure and aligns with most frontend tooling conventions.
If you omit the "type" field or set it to "commonjs", Node.js treats .js files as CommonJS by default, and you must use .mjs explicitly for ES modules. This explicit extension usage can be helpful for gradual migration or mixed environments.
In some cases, you might want to mix module types within the same project. For example, you could have:
/my-project /package.json (with "type": "module") /index.js (ES module) /legacy.cjs (CommonJS module)
Here, index.js is treated as an ES module, while legacy.cjs remains CommonJS. This allows you to isolate legacy code or dependencies that haven’t been ported yet.
Another important aspect is how Node resolves entry points. The main field in package.json points to the CommonJS entry by convention. When using ES modules, it’s recommended to use exports or module fields for explicit entry points:
{
"name": "my-esm-package",
"version": "1.0.0",
"type": "module",
"exports": {
".": "./index.js"
}
}
The exports field not only defines the entry point but also controls which files are accessible to consumers of your package. It’s an important security and encapsulation feature introduced in Node.js 12+.
If you want to write hybrid packages that support both CommonJS and ESM consumers, you need to provide dual entry points, often via conditional exports:
{
"name": "my-dual-package",
"version": "1.0.0",
"exports": {
"import": "./index.mjs",
"require": "./index.cjs"
}
}
This tells Node.js to load index.mjs when imported as an ES module, and index.cjs when required from CommonJS. This pattern is essential for libraries that want maximum compatibility.
Lastly, pay attention to file extensions and import paths. ES modules require full file extensions in import statements, unlike CommonJS:
import { something } from './utils.js'; // must include .js
Omitting the extension will result in module resolution errors. That is because ES modules use a strict spec-compliant resolver that does not infer extensions or index files automatically.
To summarize the key configuration points:
- Add
"type": "module"topackage.jsonto enable ES modules for.jsfiles. - Use
.mjsextension if you want to opt-in on a per-file basis without changingpackage.json. - Use
exportsormodulefields for explicit entry points in packages. - Include full file extensions in all import paths.
- Use conditional exports for dual CommonJS/ESM packages.
