Backend Development 21 min read

Understanding the Node.js Module System: Resolving, Loading, Wrapping, Caching and Exports

Node.js implements the CommonJS module system by using global require and module objects that, when a module is requested, resolve its absolute path, load and wrap the file, evaluate it to populate module.exports, cache the result, and handle exports, circular dependencies, JSON and native addons.

vivo Internet Technology
vivo Internet Technology
vivo Internet Technology
Understanding the Node.js Module System: Resolving, Loading, Wrapping, Caching and Exports

Node.js follows the CommonJS specification for module management, providing two built‑in modules— require and module —that can be used globally without explicit import.

When a module is required, Node.js performs five distinct steps:

Resolving : locate the target module and generate an absolute path.

Loading : determine the file type (.js, .json, .node) and read its contents.

Wrapping : wrap the module code in a function to give it a private scope.

Evaluating : execute the wrapped function, populating module.exports .

Caching : store the loaded module in require.cache so subsequent require calls return the same instance.

Simple require example:

const config = require('/path/to/file')

The above line internally triggers all five steps.

Key properties of the module object can be inspected in a REPL:

~/learn-node $ node
> module
Module { id: '
', exports: {}, parent: undefined, filename: null, loaded: false, children: [], paths: [ ... ] }

Two important attributes are id (usually the absolute file path) and paths (an ordered list of directories Node searches for modules). Example of the paths array:

~/learn-node $ node
> module.paths
[ '/Users/samer/learn-node/repl/node_modules',
  '/Users/samer/learn-node/node_modules',
  '/Users/samer/node_modules',
  '/Users/node_modules',
  '/node_modules',
  '/Users/samer/.node_modules',
  '/Users/samer/.node_libraries',
  '/usr/local/Cellar/node/7.7.1/lib/node' ]

If a module cannot be found, require throws an error:

~/learn-node $ node
> require('find-me')
Error: Cannot find module 'find-me'
    at Function.Module._resolveFilename (module.js:470:15)
    ...

Creating a node_modules folder with find-me.js allows the module to be resolved:

~/learn-node $ mkdir node_modules
~/learn-node $ echo "console.log('I am not lost');" > node_modules/find-me.js
~/learn-node $ node
> require('find-me');
I am not lost
{}

Node also resolves directories by automatically loading an index.js file, or a file specified by the main field in package.json :

~/learn-node $ mkdir -p node_modules/find-me
~/learn-node $ echo "console.log('Found again.');" > node_modules/find-me/index.js
~/learn-node $ node
> require('find-me');
Found again.
{}

Exports vs. module.exports :

exports is a reference to module.exports . Modifying exports changes the exported object, but reassigning exports does not affect module.exports .

module.exports is the actual object returned by require . Assigning a new value to module.exports determines what other modules receive.

// lib/util.js
exports.id = 'lib/util';
// index.js
exports.id = 'index';
const util = require('./lib/util');
console.log('UTIL:', util);
// Output: UTIL: { id: 'lib/util' }

Module loading is synchronous. The loaded flag on a module becomes true only after the module’s code has finished executing. Using setImmediate demonstrates this:

// In index.js
setImmediate(() => {
  console.log('The index.js module object is now loaded!', module);
});

Node handles circular dependencies by providing a partially‑filled exports object to the second module. Example:

// lib/module1.js
exports.a = 1;
require('./module2'); // circular reference
exports.b = 2;
exports.c = 3;
// lib/module2.js
const Module1 = require('./module1');
console.log('Module1 is partially loaded here', Module1);
// Running module1.js prints: Module1 is partially loaded here { a: 1 }

Beyond JavaScript files, require can load .json and native .node addons. The supported extensions are listed in require.extensions :

> require.extensions
{ '.js': [Function], '.json': [Function], '.node': [Function] }

Loading a JSON configuration:

const { host, port } = require('./config');
console.log(`Server will run at http://${host}:${port}`);
// Output: Server will run at http://localhost:8080

Native addons are compiled C++ modules (e.g., addon.node ) and can be required like any other file:

const addon = require('./addon');
console.log(addon.hello()); // prints "world"

Wrapping: before execution, Node wraps each module’s source code in a function with the signature (exports, require, module, __filename, __dirname) . This creates a private scope for the module, preventing variables from leaking globally.

require('module').wrapper
[ '(function (exports, require, module, __filename, __dirname) { ', '\n});' ]

Because the wrapper returns module.exports , whatever is assigned to that object becomes the public API of the module.

Caching: after a module is loaded, it is stored in require.cache . Subsequent require calls return the cached instance without re‑executing the module code.

// index.js
console.log('log something.');
// In REPL
> require('./index.js')
log something.
{}
> require('./index.js')
{}
> require.cache
{ '/Users/samer/index.js': Module { id: '/Users/samer/index.js', ... } }

The article concludes with a brief recap and references for further reading.

Node.jscachingModule SystemRequireCommonJSExportswrapping
vivo Internet Technology
Written by

vivo Internet Technology

Sharing practical vivo Internet technology insights and salon events, plus the latest industry news and hot conferences.

0 followers
Reader feedback

How this landed with the community

login Sign in to like

Rate this article

Was this worth your time?

Sign in to rate
Discussion

0 Comments

Thoughtful readers leave field notes, pushback, and hard-won operational detail here.