Node.js October 23, 2024 Aditya Rawas

How require() Works in Node.js: CommonJS Module Loading Explained

Node.js is built on the CommonJS (CJS) module system. The require() function is how you load modules — but what actually happens when you call it? In this deep dive, we’ll trace the full lifecycle of require() from path resolution to returned exports.


What is CommonJS?

CommonJS defines how modules are loaded and shared in Node.js. Each file is a module that exposes functionality via module.exports or exports.

// math.js
const add = (a, b) => a + b;
module.exports = add;
// app.js
const add = require('./math');
console.log(add(2, 3)); // Output: 5

When require('./math') is called, several things happen behind the scenes.


The 5 Steps of require()

  1. Resolve the module path — convert the identifier to an absolute filesystem path.
  2. Check the module cache — return the cached module if it’s been loaded before.
  3. Create a new module object — initialize a fresh module if it’s not cached.
  4. Load and execute the module — read the file, wrap it, and run it.
  5. Return module.exports — the result is passed back to the calling code.

How require() Works Internally

Here’s a simplified version of the internal implementation:

function require(moduleId) {
  // 1. Resolve to an absolute path
  const filename = Module._resolveFilename(moduleId, this);

  // 2. Return cached module if available
  if (Module._cache[filename]) {
    return Module._cache[filename].exports;
  }

  // 3. Create a new module and cache it immediately
  const module = new Module(filename);
  Module._cache[filename] = module;

  // 4. Load and execute the module
  try {
    module.load(filename);
  } catch (err) {
    delete Module._cache[filename]; // Clean up on failure
    throw err;
  }

  // 5. Return the exports object
  return module.exports;
}

Key Components


Module Wrapping and Execution

Node.js doesn’t just execute module code directly. It wraps every file’s contents in a function before running it:

(function (exports, require, module, __filename, __dirname) {
  // Your module code runs here
});

This wrapper provides each module with its own isolated scope and makes the following variables available:

VariableWhat it is
exportsShorthand reference to module.exports
requireThe require function scoped to this module
moduleThe current module object
__filenameAbsolute path of the current file
__dirnameDirectory containing the current file

This is why modules in Node.js don’t pollute the global scope — every variable you declare is local to the wrapper function.


Walkthrough: Loading a Module

// moduleA.js
const greet = () => {
  console.log('Hello, world!');
};
module.exports = greet;

When you call require('./moduleA'):

  1. Resolve path: ./moduleA/project/moduleA.js
  2. Check cache: Not found, proceed.
  3. Wrap code:
    (function (exports, require, module, __filename, __dirname) {
      const greet = () => {
        console.log('Hello, world!');
      };
      module.exports = greet;
    });
  4. Execute: module.exports is set to the greet function.
  5. Cache: /project/moduleA.js{ exports: greet } is stored.
  6. Return: greet is returned to the caller.

Module Caching and Singletons

Node.js caches every module after it’s first loaded. This means requiring the same module twice returns the same instance:

const mod1 = require('./moduleA');
const mod2 = require('./moduleA');

console.log(mod1 === mod2); // true — same reference

This singleton behavior is useful for sharing state (like database connections) across a Node.js application, since you always get the same module object.


How Module Paths Are Resolved

When you call require(identifier), Node.js resolves it in this order:

  1. Core modules (e.g., require('fs'), require('http')) — loaded immediately from the Node.js binary.
  2. Relative/absolute paths (e.g., require('./utils')) — resolved to the filesystem.
  3. node_modules lookup — if no path prefix, Node.js searches node_modules starting from the current directory, then parent directories, up to the filesystem root.

Key Takeaways

Knowing how require() works under the hood makes you a more effective Node.js developer — whether you’re debugging mysterious module behavior or designing clean module interfaces.