| |

Node.js 4 🟢 CommonJS Module System (require and module.exports)

Every Node.js file is a module. When you write code in a .js file and run it with Node.js, the runtime wraps that code in a function before executing it. This wrapper provides the require, module, and exports variables that make the module system work. Understanding this mechanism is essential because it explains how code is shared between files, why circular dependencies behave the way they do, and how the module cache prevents redundant loading.

CommonJS is the original module system for Node.js, designed before ES modules were standardized. It uses synchronous loading and a simple contract: a module declares what it exports by assigning to module.exports or exports, and other modules import those values by calling require(). Despite the rise of ES modules, CommonJS remains the dominant module format in the Node.js ecosystem, supported by millions of packages and the default for .js files in projects without "type": "module" in their package.json.

This chapter covers the module wrapper function, the require function and its resolution algorithm, the module object and module.exports, the difference between exports and module.exports, the module cache, and circular dependencies. We will also examine the properties of module that are useful for introspection and the patterns for exporting functions, objects, and classes.

Key point: Every CommonJS module is wrapped in a function that receives exports, require, module, __filename, and __dirname. The require() function resolves paths, checks the cache, executes the module on first load, and returns whatever the module assigned to module.exports.


Why the CommonJS module system exists

The global namespace problem. Early JavaScript had no module system. All code shared a single global namespace, and combining scripts from different sources risked name collisions. A variable named utils in one file could overwrite an identically named variable in another. CommonJS solves this by giving each file its own scope, isolated from every other file. Variables declared at the top level of a module are private unless explicitly exported.

The dependency management problem. Without a module system, developers had to manually ensure that scripts loaded in the correct order and that dependencies were available before use. CommonJS makes dependencies explicit: a module declares what it needs with require(), and the runtime ensures those dependencies are loaded and available before the requiring module executes.

The code reuse problem. Copying and pasting code between files is error-prone and creates maintenance burdens. Modules allow code to be written once and reused across many files. A utility function, a database connection, or a configuration object can live in one module and be imported wherever needed.

The encapsulation problem. Not everything in a module should be public. CommonJS allows a module to export only what it chooses, keeping internal implementation details private. A module might expose a function while hiding its helper functions, or expose a class while hiding its internal state management.

The load-once problem. If two modules both require the same third module, that third module should only execute once. CommonJS solves this with a module cache. The first require() executes the module and stores its exports; subsequent require() calls return the cached exports without re-executing. This ensures that shared state, such as a database connection pool, is truly shared.


a. The module wrapper function

Before a module’s code is executed, Node.js wraps it in a function that looks like this:

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

This wrapper is not something you write; Node.js adds it automatically. The five parameters are the variables that appear to be global but are actually module-scoped. exports is a reference to module.exports. require is the function for importing other modules. module is an object representing the current module. __filename is the absolute path to the current file. __dirname is the absolute path to the directory containing the current file.

You can verify this wrapping by inspecting module.wrapper or by logging arguments inside a module, though the latter is not recommended in production code:

console.log(__filename); // /path/to/current/file.js
console.log(__dirname);  // /path/to/current

The wrapper function is why top-level var declarations in a module do not become properties of global. They are local to the wrapper function. It is also why require, module, and exports are available without any import statement.

b. The require function and resolution

require() is a function that takes a module identifier and returns the module’s exports. The identifier can be a relative path, an absolute path, a core module name, or a package name:

const fs = require('fs');                 // core module
const path = require('path');             // core module
const lodash = require('lodash');         // node_modules package
const utils = require('./utils');         // relative path
const config = require('../config/app');  // parent directory

The resolution algorithm follows a defined order. For a relative or absolute path, Node.js checks for a file with the exact name, then adds extensions in order (.js, .json, .node), then treats the path as a directory and looks for package.json with a main field, then for index.js. For a bare specifier like lodash, Node.js walks up the directory tree looking for node_modules/lodash.

Core modules like fs, path, and http take precedence over packages with the same name. If you create a file named fs.js and require 'fs', you still get the core module. To require a local file with the same name, use an explicit relative path like './fs'.

A useful property of require is require.resolve(), which returns the resolved absolute path without loading the module:

console.log(require.resolve('./utils'));
// /home/user/project/utils.js

The require.cache object exposes the module cache. Each key is a resolved filename, and each value is a Module object:

console.log(Object.keys(require.cache));

c. module.exports and exports

Every module has a module object. The module.exports property is what require() returns. Initially, module.exports is an empty object {}. The exports variable is a reference to that same object.

This means the following two lines are equivalent when adding properties:

exports.greet = function() { return 'hello'; };
module.exports.greet = function() { return 'hello'; };

But they diverge when reassigning:

// This works: module.exports is reassigned
module.exports = function() { return 'hello'; };

// This does NOT work: exports now points to the old object
exports = function() { return 'hello'; };

The second example is a common mistake. Reassigning exports breaks the reference to module.exports. The require() call returns the original empty object, not the new function. The rule is: always assign to module.exports when exporting a single value, and use exports.property when adding named properties.

A module can export anything: a function, a class, an object, a primitive, or even a string. The most common patterns are exporting an object with named properties, exporting a single function, or exporting a class.

d. The module object and its properties

The module object has several useful properties beyond exports. module.id is the module’s identifier, typically the resolved filename, or '.' for the main module. module.filename is the fully resolved filename. module.loaded is a boolean indicating whether the module has finished loading. module.parent is the module that first required this one, or null if this is the entry point. module.children is an array of modules that this module has required.

console.log(module.id);       // '/path/to/file.js' or '.'
console.log(module.filename); // '/path/to/file.js'
console.log(module.loaded);   // false during loading, true after
console.log(module.parent);   // the requiring module, or null
console.log(module.children); // array of required modules

The module.paths property is an array of directories that Node.js searches when resolving bare specifiers. It starts with the current directory’s node_modules and walks up to the filesystem root:

console.log(module.paths);
// [ '/project/node_modules',
//   '/node_modules' ]

e. The module cache and load-once semantics

The first time a module is required, Node.js executes it and caches the module object in require.cache. Every subsequent require() of the same resolved filename returns the cached module.exports without re-executing the file.

This has important consequences. If a module has side effects, such as opening a database connection, those side effects happen once. If two modules require the same module, they both receive the same instance of its exports. Shared state through modules is truly shared.

You can inspect and manipulate the cache:

// Inspect the cache
console.log(Object.keys(require.cache));

// Delete a module from the cache to force re-execution
delete require.cache[require.resolve('./config')];

Deleting from the cache is useful for hot-reloading during development but is rare in production. It can lead to surprising behavior if the module has side effects or if other modules hold references to the old exports.

The cache is keyed by the resolved absolute filename, so different relative paths that resolve to the same file share the cache entry. Requiring './utils' and './utils.js' from the same directory returns the same cached module.

f. Circular dependencies

Circular dependencies occur when module A requires module B and module B requires module A. CommonJS handles this by returning the partially completed exports of the module that is currently loading.

Consider two modules:

// a.js
exports.done = false;
const b = require('./b');
console.log('in a, b.done =', b.done);
exports.done = true;
console.log('a done');
// b.js
exports.done = false;
const a = require('./a');
console.log('in b, a.done =', a.done);
exports.done = true;
console.log('b done');

When a.js runs first, it sets exports.done = false, then requires b.js. b.js sets its own exports.done = false, then requires a.js. Since a.js is already loading, the cache returns its current partial exports, where done is still false. So b.js logs in b, a.done = false. Then b.js finishes, and control returns to a.js, which logs in a, b.done = true.

The key insight is that circular dependencies work but require care. The module that is required second sees a partially constructed exports object. Design modules to avoid circular dependencies when possible; when they are unavoidable, ensure that the circular reference is used after both modules have finished loading.


Complete Example Session

// ============================================
// PART 1: EXPORTING WITH module.exports
// ============================================
// math.js — exports a single object with methods.

// math.js:
module.exports = {
    add: (a, b) => a + b,
    subtract: (a, b) => a - b,
    multiply: (a, b) => a * b,
};
// ============================================
// PART 2: REQUIRING A MODULE
// ============================================
// app.js — requires the math module.

// app.js:
const math = require('./math');
console.log(math.add(2, 3));      // 5
console.log(math.multiply(4, 5)); // 20
// ============================================
// PART 3: ADDING PROPERTIES WITH exports
// ============================================
// utils.js — using exports.property syntax.

// utils.js:
exports.greet = (name) => `Hello, ${name}`;
exports.farewell = (name) => `Goodbye, ${name}`;
// ============================================
// PART 4: EXPORTING A SINGLE FUNCTION
// ============================================
// logger.js — module.exports is a function.

// logger.js:
module.exports = function(message) {
    console.log(`[LOG] ${message}`);
};
// ============================================
// PART 5: REQUIRING A SINGLE-FUNCTION MODULE
// ============================================
// The required value is the function itself.

// app.js:
const log = require('./logger');
log('Application started');
// ============================================
// PART 6: EXPORTING A CLASS
// ============================================
// user.js — exports a class constructor.

// user.js:
class User {
    constructor(name) {
        this.name = name;
    }
    greet() {
        return `Hi, I'm ${this.name}`;
    }
}
module.exports = User;
// ============================================
// PART 7: REQUIRING A CLASS
// ============================================
// The required value is the class.

// app.js:
const User = require('./user');
const alice = new User('Alice');
console.log(alice.greet());
// ============================================
// PART 8: INSPECTING THE MODULE OBJECT
// ============================================
// module properties reveal loading metadata.

console.log(module.id);        // '.' for main module
console.log(module.filename);  // absolute path
console.log(module.loaded);    // false during load
console.log(module.parent);    // null for entry point
// ============================================
// PART 9: MODULE CACHE
// ============================================
// Same module returns the same instance.

const config1 = require('./config');
const config2 = require('./config');
console.log(config1 === config2); // true
// ============================================
// PART 10: CIRCULAR DEPENDENCY
// ============================================
// a.js and b.js require each other.

// a.js:
exports.done = false;
const b = require('./b');
console.log('in a, b.done =', b.done);
exports.done = true;

// b.js:
exports.done = false;
const a = require('./a');
console.log('in b, a.done =', a.done);
exports.done = true;

These ten parts cover the core patterns: exporting objects, functions, and classes; requiring them; inspecting the module object; the cache’s load-once behavior; and the partial-exports behavior of circular dependencies.


Quick Reference

Module Wrapper Parameters

ParameterDescription
exportsReference to module.exports
requireFunction to import modules
moduleObject representing current module
__filenameAbsolute path to current file
__dirnameAbsolute path to current directory

require Resolution Order

Specifier TypeResolution
Core module (fs)Built-in module
Relative (./utils)File, then .js, .json, .node, then directory
Absolute (/path)Same as relative after path resolution
Bare (lodash)Walk up node_modules directories

Export Patterns

PatternSyntaxRequired Value
Named propertiesexports.foo = ...Object with foo
Single valuemodule.exports = ...The assigned value
Classmodule.exports = classThe class
Functionmodule.exports = functionThe function

Module Object Properties

PropertyDescription
module.idModule identifier (filename or .)
module.filenameFully resolved filename
module.loadedWhether loading is complete
module.parentModule that first required this one
module.childrenModules required by this one
module.pathsDirectories searched for bare specifiers

Best Practices

✅ Do This:

module.exports = { add, subtract };              // Named exports object
exports.greet = (name) => `Hello, ${name}`;      // Add to exports
const utils = require('./utils');                // Relative path with ./
module.exports = class User {}                   // Export a single class
delete require.cache[require.resolve('./config')] // Hot reload during dev

❌ Don’t Do This:

exports = { add, subtract };                     // ❌ Breaks reference
const utils = require('utils');                  // ❌ Bare specifier for local file
module.exports = 'string';                       // ❌ Works, but confusing
require('./utils');                              // ❌ Missing ./, resolves to package
exports.foo = exports.bar = () => {};            // ❌ Chained assignment confusion

Common Pitfalls

PitfallWhy It HappensFix
require returns {}Reassigned exports instead of module.exportsUse module.exports = ...
Module not foundBare specifier for a local fileUse ./ prefix
Circular dependency gives partial objectModule not fully loadedUse after both modules load
Cache returns stale dataModule cached from earlier requireDelete cache entry or restart
.json require failsInvalid JSONValidate JSON syntax
Different paths, same fileCached by resolved filenameBoth return same instance

Real-World Examples

1. Configuration Module

// config.js
module.exports = {
    port: process.env.PORT || 3000,
    dbUrl: process.env.DATABASE_URL,
};

2. Utility Functions

// string-utils.js
exports.capitalize = (s) => s.charAt(0).toUpperCase() + s.slice(1);
exports.reverse = (s) => s.split('').reverse().join('');

3. Single-Function Export

// hash.js
module.exports = (input) => crypto.createHash('sha256').update(input).digest('hex');

4. Class Export

// database.js
class Database {
    connect() { /* ... */ }
}
module.exports = Database;

5. Singleton via Cache

// pool.js
module.exports = new ConnectionPool(); // cached; same pool everywhere

6. Requiring JSON

const pkg = require('./package.json');
console.log(pkg.version);

7. Conditional Require

if (process.env.NODE_ENV === 'development') {
    require('./debug-tools');
}

8. Resolve Without Loading

const path = require.resolve('lodash');
console.log(path);

9. Clearing Cache for Tests

beforeEach(() => {
    delete require.cache[require.resolve('./module-under-test')];
});

10. Inspecting Children

console.log(module.children.map(c => c.id));

Visual

The Module Wrapper

┌──────────────────────────────────────────────────────────────┐
│  HOW NODE.JS WRAPS EVERY MODULE                              │
│                                                              │
│  Your source file (utils.js):                                │
│  ┌────────────────────────────────────────────────────────┐  │
│  │  exports.greet = (name) => `Hello, ${name}`;           │  │
│  │  const path = require('path');                         │  │
│  └────────────────────────────────────────────────────────┘  │
│                          │                                   │
│                          ▼                                   │
│  Wrapped by Node.js:                                         │
│  ┌────────────────────────────────────────────────────────┐  │
│  │  (function(exports, require, module,                   │  │
│  │            __filename, __dirname) {                    │  │
│  │      exports.greet = (name) => `Hello, ${name}`;       │  │
│  │      const path = require('path');                     │  │
│  │  });                                                   │  │
│  └────────────────────────────────────────────────────────┘  │
│                          │                                   │
│                          ▼                                   │
│  Executed with:                                              │
│  exports    = module.exports                                 │
│  require    = function to load modules                       │
│  module     = { exports: {}, id, filename, ... }             │
│  __filename = '/absolute/path/to/utils.js'                   │
│  __dirname  = '/absolute/path/to'                            │
│                                                              │
│  Top-level var declarations are local to this wrapper.       │
└──────────────────────────────────────────────────────────────┘

require Resolution Algorithm

┌──────────────────────────────────────────────────────────────┐
│  HOW require() FINDS A MODULE                                │
│                                                              │
│  require('./utils')                                          │
│       │                                                      │
│       ▼                                                      │
│  Is it a core module? ──Yes──▶ Return core module            │
│       │                                                      │
│       No                                                     │
│       │                                                      │
│       ▼                                                      │
│  Is it a relative/absolute path?                             │
│       │                                                      │
│       Yes                                                    │
│       │                                                      │
│       ▼                                                      │
│  Try: ./utils                                                │
│  Try: ./utils.js                                             │
│  Try: ./utils.json                                           │
│  Try: ./utils.node                                           │
│  Try: ./utils/package.json (main field)                      │
│  Try: ./utils/index.js                                       │
│       │                                                      │
│       ▼                                                      │
│  Not found? ──▶ Throw MODULE_NOT_FOUND                       │
│                                                              │
│  For bare specifier (lodash):                                │
│  Walk up from current dir looking for node_modules/lodash    │
└──────────────────────────────────────────────────────────────┘

Module Cache and Load-Once

┌──────────────────────────────────────────────────────────────┐
│  require.cache PREVENTS RE-EXECUTION                         │
│                                                              │
│  require.cache = {                                           │
│    '/project/math.js': Module { exports: {...} },            │
│    '/project/utils.js': Module { exports: {...} },           │
│    '/project/config.js': Module { exports: {...} }           │
│  }                                                           │
│                                                              │
│  First require('./config'):                                  │
│  1. Resolve to /project/config.js                            │
│  2. Not in cache                                            │
│  3. Create Module, add to cache                             │
│  4. Execute module code                                     │
│  5. Return module.exports                                   │
│                                                              │
│  Second require('./config'):                                 │
│  1. Resolve to /project/config.js                            │
│  2. Found in cache                                          │
│  3. Return cached module.exports immediately                │
│                                                              │
│  No re-execution. Side effects happen once.                  │
└──────────────────────────────────────────────────────────────┘

exports vs module.exports

┌──────────────────────────────────────────────────────────────┐
│  exports AND module.exports REFERENCE THE SAME OBJECT        │
│                                                              │
│  Initially:                                                  │
│  ┌─────────────────┐         ┌─────────────────┐            │
│  │     exports     │────────▶│  module.exports │            │
│  └─────────────────┘         │      {}         │            │
│                              └─────────────────┘            │
│                                                              │
│  Adding properties works:                                    │
│  exports.foo = 'bar';                                        │
│  ┌─────────────────┐         ┌─────────────────┐            │
│  │     exports     │────────▶│  module.exports │            │
│  └─────────────────┘         │  { foo: 'bar' } │            │
│                              └─────────────────┘            │
│                                                              │
│  Reassigning exports breaks the reference:                   │
│  exports = { foo: 'bar' };                                   │
│  ┌─────────────────┐         ┌─────────────────┐            │
│  │     exports     │───X     │  module.exports │            │
│  └─────────────────┘         │      {}         │            │
│         │                    └─────────────────┘            │
│         ▼                                                    │
│  { foo: 'bar' }              require() returns {}            │
│                                                              │
│  Rule: use module.exports = ... for single values.           │
└──────────────────────────────────────────────────────────────┘

Summary

ItemValue
Module wrapper(function(exports, require, module, __filename, __dirname) { ... })
Module scopeEach file has its own scope; top-level vars are private
require(id)Resolves, caches, executes, returns module.exports
Core module precedencefs, path, etc. override same-named packages
Resolution orderFile → extension → directory → package.json → index.js
Cache keyResolved absolute filename
Load-onceModules execute once; subsequent requires return cached exports
exports vs module.exportsSame object initially; reassigning exports breaks the link
Circular dependenciesSecond module sees partial exports of first
require.resolve()Returns resolved path without loading

Key takeaways:

  • Every CommonJS module is wrapped in a function. The wrapper provides exports, require, module, __filename, and __dirname as local variables .
  • require() resolves, caches, and executes. The first require executes the module and caches its exports; subsequent requires return the cached value without re-execution .
  • Use module.exports for single values, exports.property for named exports. Reassigning exports breaks the reference to module.exports and returns an empty object.
  • The module cache ensures shared state is shared. Two modules requiring the same dependency receive the same instance .
  • Circular dependencies work with partial exports. The second module in the cycle sees whatever the first module had exported at the time of the require call.
  • Core modules take precedence over packages. A local file named fs.js is not loaded by require('fs'); use './fs' to load the local file.
  • require.resolve() is useful for introspection. It returns the absolute path without loading the module.
  • Manipulating the cache is rare but possible. delete require.cache[...] forces re-execution, useful for hot reloading and testing.

Remember: The CommonJS module system gives every file its own scope and a clear contract for sharing code. require() resolves a specifier to an absolute path, checks the cache, executes the module if not cached, and returns module.exports. The wrapper function provides the variables that appear global but are module-scoped, which is why top-level declarations do not leak into the global namespace. The module cache guarantees that shared dependencies are instantiated once, making it safe to hold connections, configuration, and state in modules. Circular dependencies are handled by returning partial exports, but they should be avoided when possible. Mastering CommonJS is mastering how Node.js applications are structured, because every file you write is a module and every dependency you use is loaded through require().



Stop using slow, ad-bloated tool sites! 🤮

🔎 Search “KandZ Tools” on Google to use many professional utilities for free.

KandZ.me is the ultimate minimalist hub for:
✅ Finance (Mortgage, Interest, Inflation)
✅ Tech (Base64, JSON, Dev Suite, IP)
✅ Health (BMI, BMR, TDEE)
✅ Productivity (Timer, Workspace, QR)

⚡️ Fast & Private
🔒 No data leaves your device
💎 100% Free

🔗 Use it now: https://tools.kandz.me
🔖 Bookmark it—you’ll need it later!