Understanding How `require` Works in CommonJS: A Deep Dive into Node.js Module Loading

Written By
Aditya Rawas
Published
a month ago
requireCommonJSNode.jsmodule loadingdeep diveunderstandinghow require works

Node.js is built on the CommonJS (CJS) module system, which enables developers to structure their applications using modules. This modularity is achieved through the `require` function, a key feature that allows one module to load and use functionality from another. But how does `require` actually work under the hood? In this blog, we’ll explore the mechanics of `require`, look into its internal implementation, and see how Node.js loads and manages modules efficiently.

What is CommonJS?

CommonJS is a module system used primarily in Node.js. It defines how modules are loaded and managed. Modules in Node.js are single files that encapsulate functionality and expose it to other modules through module.exports or exports.

Here’s a basic example:

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

In this example, the require function loads the math.js module and returns the exports object, which contains the add function.

The Basic Steps of require

When you use require('./math') in your code, several things happen under the hood:

  1. Resolve the Module Path: Node.js first resolves the module path or identifier.
  2. Check for Cached Modules: If the module has been loaded before, Node.js will fetch it from the cache.
  3. Create a New Module Object: If the module isn’t cached, Node.js creates a new module object.
  4. Load the Module: Node.js reads the module’s file, wraps its contents in a function, and executes it.
  5. Return the exports Object: The result of module.exports is returned and can be used in the requiring file.

How require Works Internally

Now let’s dive into a simplified version of the internal implementation of require in Node.js.

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

  // 2. Check if the module is cached
  if (Module._cache[filename]) {
    return Module._cache[filename].exports;
  }

  // 3. Create a new module object
  const module = new Module(filename);

  // Cache the module
  Module._cache[filename] = module;

  // 4. Load the module (read file contents, compile, and execute)
  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 of require:

  1. Module._resolveFilename: This method takes the module identifier and resolves it to an absolute path on the filesystem.

  2. Module._cache: Node.js caches modules once they’re loaded. If the same module is required again, the cached version is returned, improving performance by avoiding redundant work.

  3. module.load: The module loading function, which reads the file, wraps it in a function, and evaluates it.

  4. module.exports: This is the object that is returned by the require function. The module author defines what is exported through this object.

Module Wrapping and Execution

When Node.js loads a module, it doesn’t just execute the file directly. It wraps the contents of the module in a special function to provide the necessary context, like require, module, exports, __dirname, and __filename.

Here’s what happens behind the scenes:

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

This function ensures that each module has its own scope, preventing it from polluting the global scope. As a result, modules in Node.js behave like isolated units with controlled environments.

Example: Loading a Module

Let’s break down what happens when a module is loaded:

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

When we require this module:

const greet = require('./moduleA');
greet(); // Outputs: Hello, world!

Here’s what happens internally:

  1. Resolve the Path: Node.js resolves the path ./moduleA.js.

  2. Check Cache: Node.js checks if the module is cached. If not, it proceeds.

  3. Wrap the Code: The code from moduleA.js is wrapped in a function like:

    (function (exports, require, module, __filename, __dirname) {
      const greet = () => {
        console.log('Hello, world!');
      };
      module.exports = greet;
    });
    
  4. Execute the Wrapped Code: The function is executed, and module.exports is set to the greet function.

  5. Cache the Module: The module is cached for future use.

  6. Return module.exports: The greet function is returned and can now be used in the requiring file.

Module Caching and Singletons

Node.js caches every module after it is loaded. This caching mechanism ensures that when you require the same module again, you get the same instance. This is important because it means that modules act like singletons — only one instance of the module exists.

// Example of module caching
const mod1 = require('./moduleA');
const mod2 = require('./moduleA');

console.log(mod1 === mod2); // true

Since mod1 and mod2 are references to the same cached module, they are equal.

How Modules are Resolved

When you use require, Node.js resolves the module using the following steps:

  1. Core Modules: If the module is a built-in core module (like fs, http), it’s loaded immediately.

  2. File Path: If you provide a relative or absolute path, Node.js will load the corresponding file.

  3. node_modules Lookup: If the module name doesn’t match a file or path, Node.js will look for it in node_modules directories, starting from the current directory and moving up the directory tree.

Conclusion

The require function is an essential part of Node.js that enables modularity through the CommonJS module system. Internally, it handles everything from resolving paths to loading and caching modules, all while ensuring efficient and secure execution of code.

Understanding how require works not only helps you write better Node.js applications but also gives you a glimpse into the inner workings of module loading, caching, and execution in Node.js. By taking advantage of this system, you can structure your applications cleanly and improve maintainability.

Whether you’re building a small utility or a large-scale application, Node.js modules and the require function will be foundational tools in your development process.