Node.js Module Systems Explained: CommonJS vs ES Modules in Node & Browser.
(Node Series Part 4)
In this section, I will cover different types of module imports in Node and the browser.
Module
In simple terms, it is a block of reusable code.
A module is just a self-contained chunk of code with its scope that:
- Defines something reusable (like functions, classes, or objects).
- Export what it wants to share.
- Imports what it needs from other modules.
🔎 Why do we use modules?
Without modulesYou’d write all your code in one giant file or spread it across scripts, polluting the global scope, which causes:
- naming collisions,
- difficult maintenance,
- hard-to-reuse code,
- and spaghetti dependencies.
Modules solve this by letting you break your app into small, focused, isolated units — each with clear inputs/outputs.
✅ Key properties of a module
- Has its own private scope (variables inside don’t leak globally).
- Can import other modules.
- Can export specific things for others to use.
- Makes large codebases organized, reusable, and maintainable.
IIFE
It was the primary method before modules were introduced to avoid naming collisions. it created its own private scope
(function(){
})()Here is an example where the same variable name is used and collision is avoided by IIFE
Now this was not scalable, so various ways do define modules were introduced.
AMD(Asynchronous Module Defintion) (Browser Only)
You define to create a private space to create a module. This was the first concept of the module that was introduced.
To work with AMD, you need a third-party library.
<script src="https://cdnjs.cloudflare.com/ajax/libs/require.js/2.3.6/require.min.js"></script>You write your module code in a separate file. load and consume it using AMD.
Now, to create your private module
define([],fucniton(){
})
// [] - here you can add dependency
// codeHow to consume it
require([],function(){
})
// [] - here you can add dependency
// codeNow, here I have created two modules, one is log(logger.js), which just logs something that is passed, and the other is math(math.js)
//math.js
define([], function () {
return {
add: (a, b) => {
return a + b;
},
sub: (a, b) => {
return a - b;
},
};
});
// logger.js
define([], function () {
return {
log: function (message) {
console.log("output", message);
},
};
});
Inside main.js, I am using both modules
require(["math", "logger"], function (math, logger) {
var result = math.add(1, 2);
logger.log(result);
});
// import dependecny and then pass it in funcitona as arg
// to use it.Rule: the file name shall be the same as the dependency module name you are using it will automatically search the file name.
Later, when the node was introduced, AMD was not scalable, so another method for writing modules was introduced.
CommonJS (.cjs)(Node Only)
In Node.js, all files are modules. As soon as u create a new file, Node environment automatically creates it as a module.
`require` to import. To include other’s packaged
`module.exports` to export. To make your code available to others.Module is a keyword provided by Node.js.
Here I have demonstrated an example of a common JS module.
//calc.js
function add(a, b) {
return a + b
}
function sub(a, b) {
return a - b
}
function mul(a, b) {
return a * b
}
function div(a, b) {
return a / b
}
module.exports = {
add, sub, mul, div
}Now I will use it in my index.js
const calc=require('./calc.js')
console.log(calc)Make sure inside package.json your type is common, as u are using the common module system
"type":"commonjs",Here is the output
Now all functions are available in your index file.
So now, how do module.exports and require come from?
Node wraps every file you create in an IIFE.
Here, you created a module and exported a function
const greet = () => {
console.log("Hello!");
};
module.exports = greet;Now node automatically wraps it in an IIFE
(function (exports, require, module, __filename, __dirname) {
//your code
const greet = () => {
console.log("Hello!");
};
module.exports = greet;
//
});
`exports` – a shorthand reference to module.exports
`require` – the function to import other modules
`module` – the current module object, including module.exports
`__filename` – full path of the current file
`__dirname` – directory of the current fileHere you get require and exports (same as module.exports).
UMD (Universal Module Definition)
UMD (Universal Module Definition) is a pattern for writing JavaScript modules that work everywhere:
- ✅ in AMD loaders (like RequireJS)
- ✅ in CommonJS environments (like Node.js)
- ✅ in browsers without any module loader (via global variables)
UMD wraps your module in a way that detects the environment and exports accordingly, making it truly universal.
🔎 Why use UMD?
If you’re writing a library you want others to use anywhere (Node, AMD, or plain <script>), UMD is perfect.
Here is a simple example where UMD is created by the module coder, while u directly consume it in any way you want but while creating a module u make sure it will work in node and browser both
(function (root, factory) {
if (typeof define === 'function' && define.amd) {
// AMD (RequireJS)
define(['dependency'], factory);
} else if (typeof exports === 'object') {
// CommonJS (Node.js)
module.exports = factory(require('dependency'));
} else {
// Browser global
root.MyModule = factory(root.Dependency);
}
}(typeof self !== 'undefined' ? self : this, function (dependency) {
// Module implementation
let privateCounter = 0;
function privateIncrement() {
privateCounter++;
}
return {
increment: function() {
privateIncrement();
return privateCounter;
},
getMessage: function() {
return `count is ${privateCounter}`
},
reset: function() {
privateCounter = 0;
return 'Counter reset';
}
};
}));Different uses of this module
1. Browser Global (without module loader)
<script src="path/to/dependency.js"></script>
<script src="path/to/myModule.js"></script>
<script>
console.log(MyModule.version); // '1.0.0'
MyModule.increment();
console.log(MyModule.getMessage());
</script>2. AMD (RequireJS)
require(['myModule'], function(myModule) {
myModule.increment();
console.log(myModule.getMessage());
});3. CommonJS (Node.js)
const myModule = require('./myModule');
const dependency = require('dependency');
myModule.increment();
console.log(myModule.getMessage());ES Module (ESM (.mjs)) (Browser/Node)
In Browser
I have created math.js with 2 functions, to export anything we have a predefined export keyword
Get Shubham Gupta’s stories in your inbox
Join Medium for free to get updates from this writer.
Named Export: The first way is to export each function individually.
export function add(a, b) {
return a + b;
}
export function sub(a, b) {
return a - b;
}
// you can also export variables in same way
export const MYVAR='value'Now, when u work in ES module, unlike require, you have to specify the type of the module while using the import keyword
<script type="module" src="main.js" />Require.js automatically handles this, and also,o while working on that, you don't have to mention the file extension, but in ES module, you have to explicitly mention the file extension
import { add } from "./math.js";
console.log(add(2, 3));While working in React, u do not need to write the extension, because the webpack handles that part and you can directly use it.
Default Export: Now, there is another way to export using the default keyword
export default Here is an example of the above file with default export
function add(a, b) {
return a + b;
}
function sub(a, b) {
return a - b;
}
const MYVAR = "value";
export default {
add,
sub,
MYVAR,
};Now, how do we consume it
import calc from "./math.js";
console.log(calc, calc.add(2, 3));You can have both default and named export and use them
In Node
For this, we specify the type in package.json At one time, only one type of module system can be used
"type": "module",Now let’s use calc.js using ES module
export function add(a,b) {
return a+b
}
export function sub(a,b) {
return a-b
}
function mul(a,b) {
return a*b
}
function div(a,b) {
return a/b
}
export default {
add,sub,mul,div
}Now you use it in the same way, make sure to use the file extension
import { add,sub } from "./calc.js";
let res=add(36,33)
console.log(res)We also have different variations of import
// here you import all named exports value and assign them a wrapper name
import * as test from "./my-module.js"
// importing specifc module
import {data}from "./my-module.js"
// default import
import data from "./my-module.js"
// renaming imported value
import { foo as myFoo } from './my-module.js';
// importing both named and default exports
import myDefault, { foo, bar } from './my-module.js';
// importing entire module , whatever code is there will be executed.
import './my-module.js';
// ways to export named and default
export const
export defaultFew Design Patterns in ES Module
Re-export everything from another module
export * from './module.js';
// for named exports only
index.jsRe-export specific named exports
export { foo, bar } from './module.js';Re-export default export as a named export
export { default as myDefault } from './module.js';
// white-labelingRe-export the default export directly as your own default
export { default } from './module.js';What’s Missing
__filename and __dirname. In the ES module, we don’t have this, so we use a package for this.
// ESM alternative for __filename and __dirname
import { fileURLToPath } from 'url';
import { dirname } from 'path';
const __filename = fileURLToPath(import.meta.url);
const __dirname = dirname(__filename);
console.log(__filename);
console.log(__dirname);We don't have to manually type the path , always use inbuilt functions
Dynamic Import
import(./mymodule.js).then((module)=>{
console.log(module)
)Circular Dependency
Circular dependencies occur when two or more modules depend on each other directly or indirectly, creating a loop (Module A → Module B → Module A). While JavaScript’s module systems (CommonJS, ES Modules) can handle some cases, circular dependencies often lead to initialization issues, partial exports, or runtime errors. To avoid problems, refactor code to eliminate cycles by:
- Moving shared logic to a third module
- Using dependency injection instead of direct imports
- Delaying imports inside functions rather than at the top level
While in common JS it moves from bottom up
CommonJS vs ES Module
✨ ESM runs in Strict Mode
✨File extension
index.js and file extension get auto-resolved in CommonJS using (require), but not in ES modules.
Although this can be resolved in the ES Module using various methods
Node Loader Plugin
`npm install @node-loader/resolve-extension`
Run using:
`node --loader=@node-loader/resolve-extension ./src/index.js`Use Vite
npm create vite@latest my-project -- --template vanilla
cd my-project
npm install
npm run devUse webpack for ESM
npm install --save-dev webpack webpack-cliconst path = require('path');
module.exports = {
entry: './src/index.js',
mode: 'development',
output: {
filename: 'bundle.js',
path: path.resolve(__dirname, 'dist'),
},
resolve: {
extensions: ['.js'], // allows extensionless imports
},
};Build with webpack : `npx webpack`
Run the bundle `node dist/bundle.js`✨ ESM is async
- ✅ CommonJS: Always synchronous loading.Loads files on-demand during runtime, which can lead to partial loads in circular dependencies.
- ✅ ESM: Fully asynchronous — they fetch modules over the network, parse imports first (hoisted), guaranteeing dependencies are ready.
// ESM:
1. Parse imports → 2. Fetch dependencies → 3. Run code
// CommonJS:
1. Run code → 2. Pause to load `require()` → 3. Continue Imagine 3 modules (A.js, B.js, C.js), each taking 1 second to load.
//ESM
[ Main Module ]
↓ (async)
┌─────┴─────┐
│ import A │ (1s)
│ import B │ (1s) ← Parallel fetch
│ import C │ (1s)
└─────┬─────┘
↓ (executes after ALL resolve)
[ Code Runs ] (Total: ~1s)// Common
[ Main Module ]
|
↓ (sync)
[ require(A) ] → (1s) → [A.js]
|
↓ (blocks until A finishes)
[ require(B) ] → (1s) → [B.js]
|
↓ (blocks until B finishes)
[ require(C) ] → (1s) → [C.js]
|
↓
[ Code Runs ] (Total: ~3s)✨ Interoperability
CJS in ESM: You can use CommonJS in ES module using this code, not directly
// esm-file.mjs (ES module)
import { createRequire } from 'node:module';
const require = createRequire(import.meta.url);
const cjsModule = require('./cjs-module.js');
console.log('Required from CJS:', cjsModule);CJS in ESM with direct import, but the file shall have ( .cjs) extension
// In an ESM file (.mjs or "type": "module" package.json)
import cjsModule from './module.cjs'; // Default import
import { namedExport } from './module.cjs'; // Named importESM in CJS: It can also be done using the code below
In such case, make sure you rename your file to .mjs
// cjs-file.js (CommonJS)
(async () => {
const esmModule = await import('./esm-module.mjs');
console.log('Imported from ESM:', esmModule.default);
})();What to use, CommonJS or ESM?
If starting on new prefer ESM, while existing code is already present in Common JS so you should know both of them.
If you’d like to be in the know, subscribe, clap, like and share. Cheers!







