Chapter 1.4☕ 16 min read

Modules System — CommonJS vs ES Modules

Modules let you split your code into separate files. Every file is a module in Node.js.

01Why Modules? — Biryani Recipe Alag Karna

Imagine making Hyderabadi dum biryani. You don't just dump all ingredients in one pot randomly. You have separate recipes for:

  • Rice preparation — basmati ko bhigona, boil karna
  • Chicken marination — masala, dahi, mirchi lagana
  • Dum cooking — slow cook with dough seal
  • Garnish — biryani ka top layer, fried onions

Each recipe is a separate module. You follow the rice recipe, then the chicken recipe, then combine them. If the chicken recipe changes, you don't need to rewrite the whole biryani process — just update one module.

Modules in Node.js work exactly the same way:

  • Each file is a module (a separate recipe)
  • You export what you want to share (this recipe's output)
  • You import what you need from other modules (use other recipes)
  • Modules keep code organized, reusable, and maintainable

Node.js has two module systems:

  • CommonJS (CJS) — The original system. Uses require() and module.exports. Default in Node.js.
  • ES Modules (ESM) — The modern JavaScript standard. Uses import and export. Requires "type": "module" in package.json.

Both are widely used. You'll see both in real projects. Understanding both is essential.

02CommonJS — require() and module.exports

CommonJS is the default module system in Node.js. It's been there since day one. It uses:

  • require(path) — to import a module
  • module.exports — to export from a module

Example — menu.js (the module):

// menu.js — Chai tapri ka menu

// Private variable — not accessible outside this file
const SECRET_RECIPE = 'Ginger + Elaichi + Adrak ka secret mix';

// Public — accessible via require
const menu = {
  chai: { price: 10, types: ['ginger', 'elaichi', 'special'] },
  biscuit: { price: 5, types: ['parle-g', 'marie'] },
  samosa: { price: 15 }
};

function getMenu() {
  return menu;
}

function getPrice(item) {
  return menu[item]?.price ?? -1;
}

// Export only what you want to share
module.exports = { getMenu, getPrice, menu };

server.js (importing the module):

// server.js — Importing our menu module
const { getMenu, getPrice } = require('./menu.js');

console.log(getMenu());
// Output: { chai: { price: 10, ... }, biscuit: {...}, samosa: {...} }

console.log('Chai ka price:', getPrice('chai'));
// Output: Chai ka price: 10

// SECRET_RECIPE is NOT accessible here — it's private to menu.js
console.log(SECRET_RECIPE); // ❌ ReferenceError: SECRET_RECIPE is not defined

How require() resolves modules:

  • require('./menu') or require('./menu.js') — Relative path (your file)
  • require('express') — Node.js looks in node_modules/express
  • require('fs') — Built-in Node.js module (no path needed)

The require() function is synchronous. It reads the file, executes it, and returns module.exports. The whole process happens at runtime, when the require() line is executed.

03module.exports vs exports — Same Bowl, Different Label

This is a common source of confusion. Let's clear it up with a biryani pot analogy.

module.exports and exports initially point to the same empty pot. Think of them as two labels on the same biryani pot:

// Initially: module.exports and exports both point to SAME object
// Like two labels on the same biryani pot
console.log(module.exports === exports); // true

// ✅ This works — adding items to the pot
exports.chai = 'ginger';
exports.price = 10;
// Both exports and module.exports now have { chai: 'ginger', price: 10 }

// ❌ This BREAKS the connection — you gave 'exports' a new pot
exports = { chai: 'special' };
// Now exports points to a NEW object, module.exports still has the old one!
// require() returns module.exports, not exports!

// ✅ Always use module.exports for assignment
exports.addItem = 'works';     // ✅ Adding properties to exports is safe
module.exports = { entire: 'object' }; // ✅ Replacing module.exports is safe
module.exports.single = 'prop'; // ✅ Also safe

Rule of thumb:

  • Adding properties to exports is fine: exports.item = 'value'
  • Reassigning exports breaks the connection: exports = {...}
  • Always use module.exports if you're replacing the whole object ✅

In practice, most developers just use module.exports for everything — it's simpler and avoids this confusion entirely.

04ES Modules — import and export

ES Modules (ESM) are the modern JavaScript standard for modules. They use import and export keywords, just like in browser JavaScript.

Enabling ES Modules: Add "type": "module" to your package.json, or use the .mjs file extension.

// package.json
{
  "type": "module",   // ← This enables ES Modules for all .js files
  "name": "chai-point"
}

Named exports vs Default export:

// 🔶 Named exports (you can have multiple)
export const chaiTypes = ['ginger', 'elaichi', 'special'];
export const price = 10;
export function makeChai(type) {
  return `Making ${type} chai...☕`;
}

// 🔷 Default export (only ONE per file)
export default {
  name: 'Chai Tapri',
  owner: 'Raju bhai'
};

Importing ES Modules:

// Import named exports (curly braces = destructuring)
import { chaiTypes, price, makeChai } from './menu.js';

// Import default export
import chaiTapri from './menu.js';

// Import everything as a namespace
import * as menu from './menu.js';
console.log(menu.chaiTypes); // ['ginger', 'elaichi', 'special']

// Rename imports
import { chaiTypes as types, price as rate } from './menu.js';

CommonJS vs ES Modules — Key Differences:

FeatureCommonJS (require)ES Modules (import)
LoadingSynchronousAsynchronous
DefaultNode.js defaultNeed "type": "module"
File extension.js (default) or .cjs.js (with type:module) or .mjs
Top-level thismodule.exportsundefined
Static analysisNo (runtime)Yes (compile-time)
Dynamic importAnywhereimport() function

Dynamic import() — works in both systems:

// Dynamic import — useful for conditional loading
if (userWantsChai) {
  const chaiModule = await import('./chai.js');
  chaiModule.makeChai();
}

ES Modules are the future. New projects should prefer ESM. But CommonJS is everywhere in existing code and older libraries, so you must understand both.

05Module Caching — Ek Baar Banao, Hamesha Use Karo

Module caching is one of Node.js's most important features — and a common source of surprise.

The rule: Modules are loaded ONLY ONCE. The first time you require() a module, Node.js executes it and caches the result. Every subsequent require() returns the SAME cached instance.

Singleton behavior — Chai Tapri example:

// counter.js — A simple counter module
let count = 0;

module.exports = {
  increment() { return ++count; },
  getCount() { return count; }
};

// app.js — Using the module multiple times
const counter1 = require('./counter.js');
const counter2 = require('./counter.js');

console.log(counter1 === counter2); // true — Same object!

counter1.increment(); // count = 1
counter1.increment(); // count = 2
console.log(counter2.getCount()); // 2 — Because counter1 and counter2 are the SAME instance

// This is called a SINGLETON pattern — one instance shared everywhere

Why caching matters:

  • Performance: Files are read and executed only once. 1000 requires = 1 execution + 999 cache hits.
  • State sharing: Database connections, configuration objects — shared across all files.
  • Surprise: If your module has mutable state, changes in one file affect ALL files that require it.

Clearing the cache (advanced, rarely needed):

// Delete a module from cache — forces re-execution on next require
delete require.cache[require.resolve('./myModule.js')];
const freshModule = require('./myModule.js'); // Re-executes!

// ⚠️ Warning: Avoid in production. Only use in testing.
// In production, let Node.js handle caching automatically.

Module caching with ES Modules: ES Modules also cache imports. But the caching behavior is identical — modules are evaluated once, and subsequent imports return the same instance.

Key Takeaways

  • ✅ Every file in Node.js is a module. Variables are private to the file by default.
  • ✅ CommonJS: module.exports to export, require() to import. Default in Node.js.
  • ✅ ES Modules: export / export default to export, import to import. Modern standard.
  • ✅ module.exports and exports initially point to the same object — don't reassign exports!
  • ✅ require() resolves: relative path → node_modules → built-in modules.
  • ✅ Modules are cached after first load — every subsequent require() returns the same instance (singleton).
  • ✅ Use "type": "module" in package.json to enable ES Modules. Use .mjs extension as an alternative.
Course Search
Search across all chapters & stages
📖

Search the course

Type any topic — branching, stash, rebase, hooks — and jump straight to that chapter.

merge branchesgit stashundo commitrebase