Difficulty: Beginner
Explain the difference between CommonJS and ES Modules in Node.js. When would you use each?
Node.js supports two module systems: CommonJS (CJS) and ES Modules (ESM). CommonJS uses require() and module.exports, is synchronous, and has been the default since Node.js was created. ES Modules use import/export syntax, are asynchronous, support static analysis and tree-shaking, and are the modern standard.
CommonJS loads modules synchronously at runtime, meaning require() can be called conditionally inside functions. ES Modules are parsed statically before execution, enabling tools to determine imports and exports at build time for optimizations like tree-shaking.
To use ESM in Node.js, either set "type": "module" in package.json or use the .mjs extension. For CJS in an ESM project, use the .cjs extension. You can import CJS modules from ESM, but not easily import ESM from CJS (requires dynamic import).
Most modern Node.js projects and frameworks use ESM. TypeScript projects often compile to CJS but the ecosystem is steadily moving toward ESM as the default.
// math.js - exporting
function add(a, b) { return a + b; }
function multiply(a, b) { return a * b; }
module.exports = { add, multiply };
// or: exports.add = add;
// app.js - importing
const { add, multiply } = require('./math');
console.log(add(2, 3));
// Conditional require (CJS only)
if (process.env.NODE_ENV === 'development') {
const debug = require('./debug-tools');
debug.enable();
}
// require.cache - modules are cached after first load
console.log(Object.keys(require.cache));
CJS is synchronous and cached. require() can be called anywhere, including conditionally. Modules are singletons - loaded once and cached.
// math.mjs (or .js with "type": "module" in package.json)
export function add(a, b) { return a + b; }
export function multiply(a, b) { return a * b; }
export default class Calculator { /* ... */ }
// app.mjs - importing
import Calculator, { add, multiply } from './math.mjs';
import { readFile } from 'fs/promises';
import { fileURLToPath } from 'url';
import { dirname } from 'path';
// __dirname equivalent in ESM
const __filename = fileURLToPath(import.meta.url);
const __dirname = dirname(__filename);
// Dynamic import (works in both CJS and ESM)
const module = await import('./dynamic-module.mjs');
console.log(add(2, 3));
ESM uses static import/export. Note __dirname is not available in ESM - you must derive it from import.meta.url.
// package.json for ESM project
{
"name": "my-app",
"type": "module",
"exports": {
".": {
"import": "./dist/index.mjs",
"require": "./dist/index.cjs"
}
}
}
// Dual package support
// index.mjs - ESM entry
export function greet(name) {
return `Hello, ${name}!`;
}
// index.cjs - CJS entry
module.exports.greet = function(name) {
return `Hello, ${name}!`;
};
The exports field in package.json lets you provide both CJS and ESM entry points. This is how libraries support both module systems.
CommonJS, ES Modules, require, import/export