Skip to content
Plurafy

Module System and CommonJS vs ES Modules

Understanding Node.js module system and differences between CommonJS and ES modules

Your project has mixed CommonJS and ES module code. Some modules import fine, others throw errors. How do you organize and migrate between module systems?

Node.js supports two module systems: CommonJS (default) and ES Modules (modern). Understanding how each works and how to interoperate them is essential for Node.js development.

// math.js - Define exports
module.exports = {
add: (a, b) => a + b,
subtract: (a, b) => a - b
};
// app.js - Require modules
const math = require('./math');
console.log(math.add(5, 3)); // 8
// Partial exports
module.exports.multiply = (a, b) => a * b;
// Or assign to exports object
exports.divide = (a, b) => a / b;
// ⚠️ Note: Don't mix exports and module.exports!
module.exports = { /* ... */ };
exports.something = null; // Won't work as expected
// math.mjs or with "type": "module" in package.json
// Named exports
export const add = (a, b) => a + b;
export const subtract = (a, b) => a - b;
// Default export
export default {
multiply: (a, b) => a * b,
divide: (a, b) => a / b
};
// app.mjs - Import modules
import math, { add, subtract } from './math.mjs';
console.log(add(5, 3)); // 8
console.log(math.multiply(5, 3)); // 15
// Import all as namespace
import * as operations from './math.mjs';
console.log(operations.add(5, 3)); // 8
// Conditional imports
if (someCondition) {
import('./expensive-module.mjs').then(mod => {
// Use module
});
}
// package.json
{
"name": "my-app",
"version": "1.0.0",
"type": "module", // Use ES modules
"exports": {
".": "./src/index.js",
"./utils": "./src/utils/index.js",
"./cli": "./src/cli.js"
}
}
// src/index.js - Main entry point
export { default as Calculator } from './calculator.js';
export * from './utils/index.js';
// src/calculator.js
export default class Calculator {
add(a, b) {
return a + b;
}
subtract(a, b) {
return a - b;
}
}
// src/utils/index.js
export { formatNumber } from './format.js';
export { validateInput } from './validation.js';
// src/utils/format.js
export const formatNumber = (num, decimals = 2) => {
return num.toFixed(decimals);
};
// src/utils/validation.js
export const validateInput = (input) => {
return typeof input === 'number' && !isNaN(input);
};
// Usage
import Calculator, { formatNumber, validateInput } from './index.js';
// ✅ Import CommonJS from ES Module (Node.js 12+)
import math from './math.cjs'; // CommonJS file
// ⚠️ Named imports from CommonJS require namespace
import * as math from './math.cjs';
console.log(math.add(5, 3)); // 8
// ✅ Use createRequire for full compatibility
import { createRequire } from 'module';
const require = createRequire(import.meta.url);
const config = require('./config.json');
const helper = require('./helper.cjs');
// ❌ import/export syntax not available in CommonJS
// import Calculator from './calculator.mjs'; // Won't work
// ✅ Use dynamic import (available in CommonJS)
async function loadModule() {
const { Calculator } = await import('./calculator.mjs');
return new Calculator();
}
loadModule().then(calc => {
console.log(calc.add(5, 3)); // 8
});
// Use in top-level
(async () => {
const lib = await import('./lib.mjs');
console.log(lib.doSomething());
})();
// CommonJS caches modules in require.cache
const math1 = require('./math');
const math2 = require('./math');
console.log(math1 === math2); // true - same instance
// Clear cache if needed
delete require.cache[require.resolve('./math')];
const math3 = require('./math'); // Fresh instance
// ES Modules also cache, but no direct access to cache
import mod1 from './module.mjs';
import mod2 from './module.mjs';
// mod1 === mod2 - same instance
src/
// ✅ Group related functionality
// auth/
// index.js - Main auth export
// jwt.js - JWT handling
// password.js - Password hashing
// database/
// index.js
// connection.js
// queries.js
// src/auth/index.js
export { default as JWTManager } from './jwt.js';
export { default as PasswordManager } from './password.js';
// src/index.js - Single entry point
export * from './auth/index.js';
export * from './database/index.js';
// ✅ Use barrel exports for cleaner imports
// Instead of: import { add } from './utils/math.js'
// Use: import { add } from './utils'
// (if utils has index.js exporting math.js)
// ✅ Avoid circular dependencies
// auth/index.js
export { default as AuthService } from './service.js';
// auth/service.js
import UserRepository from '../database/user.js'; // Different module
// Good: No circular dependency
// ❌ Bad: Circular dependency
// user.js imports from auth/service.js
// auth/service.js imports from user.js
// ✅ Lazy load heavy modules
class DataProcessor {
process() {
// Import only when needed
const heavyLib = require('./heavy-library');
return heavyLib.process(this.data);
}
}
// ✅ Use import.meta for module-specific info
console.log(import.meta.url); // file URL of current module
console.log(import.meta.main); // true if main module
// ✅ Minimize module scope
// Bad: Everything at module scope
app.use('/api', routes);
app.use(middleware);
// Good: Code executes only when imported
export function setupRoutes(app) {
app.use('/api', routes);
}
export function setupMiddleware(app) {
app.use(middleware);
}