Sitemap

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:

  1. Defines something reusable (like functions, classes, or objects).
  2. Export what it wants to share.
  3. 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

  1. Has its own private scope (variables inside don’t leak globally).
  2. Can import other modules.
  3. Can export specific things for others to use.
  4. 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

Press enter or click to view image in full size

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
// code

How to consume it

require([],function(){

})
// [] - here you can add dependency
// code

Now, 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.
Press enter or click to view image in full size

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 file

Here you get require and exports (same as module.exports).

Press enter or click to view image in full size

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));
Press enter or click to view image in full size

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));
Press enter or click to view image in full size

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 default

Few Design Patterns in ES Module

Re-export everything from another module

export * from './module.js';
// for named exports only
index.js

Re-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-labeling

Re-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:

  1. Moving shared logic to a third module
  2. Using dependency injection instead of direct imports
  3. 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 dev

Use webpack for ESM

npm install --save-dev webpack webpack-cli
const 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 import

ESM 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);
})();
Press enter or click to view image in full size

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!