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()
- Resolve the module path — convert the identifier to an absolute filesystem path.
- Check the module cache — return the cached module if it’s been loaded before.
- Create a new module object — initialize a fresh module if it’s not cached.
- Load and execute the module — read the file, wrap it, and run it.
- 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._resolveFilename: Converts'./math'into an absolute path like/home/user/project/math.js.Module._cache: A key-value store of loaded modules by absolute path. Avoids redundant loading.module.load: Reads the file contents, wraps them in a function, and executes the result.module.exports: The object returned to whoever calledrequire().
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:
| Variable | What it is |
|---|---|
exports | Shorthand reference to module.exports |
require | The require function scoped to this module |
module | The current module object |
__filename | Absolute path of the current file |
__dirname | Directory 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'):
- Resolve path:
./moduleA→/project/moduleA.js - Check cache: Not found, proceed.
- Wrap code:
(function (exports, require, module, __filename, __dirname) { const greet = () => { console.log('Hello, world!'); }; module.exports = greet; }); - Execute:
module.exportsis set to thegreetfunction. - Cache:
/project/moduleA.js→{ exports: greet }is stored. - Return:
greetis 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:
- Core modules (e.g.,
require('fs'),require('http')) — loaded immediately from the Node.js binary. - Relative/absolute paths (e.g.,
require('./utils')) — resolved to the filesystem. - node_modules lookup — if no path prefix, Node.js searches
node_modulesstarting from the current directory, then parent directories, up to the filesystem root.
Key Takeaways
require()is a synchronous, cached module loader — it reads the file once and caches the result.- Modules are singletons in Node.js — every
require()call for the same module returns the same exported object. - The module wrapper gives each file its own scope via
__filename,__dirname,exports, andmodule. - Understanding this internals helps you reason about module state, circular dependencies, and why mutation of
module.exportsmatters.
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.