Back to Interview PrepGet interview ready with AILearn each topic the way interviewers ask it, then answer like you would in the interview.
Course progress0/16 chapters · 0%

Chapter 2 of 17

~12 min

Modules & CommonJS

CommonJS is the module system Node.js has used from the start: every file is its own module, and `require`/`module.exports` control what gets shared between them. Understanding how resolution and caching work explains a lot of real-world module bugs.

require, module.exports and exports

Each file Node.js loads is wrapped in a function that receives its own `module`, `exports`, `require`, `__filename`, and `__dirname` - so variables declared in one file never leak into another. Whatever you assign to `module.exports` is exactly what a caller receives back from `require('./yourFile')`.

`exports` is simply a variable that starts out pointing at the same object as `module.exports`. Attaching properties to it (`exports.add = ...`) works because it mutates the shared object, but reassigning `exports` entirely (`exports = { add: ... }`) only rebinds the local variable - `module.exports` still points at the original object, so the reassignment is silently lost.

index.js
// math.js
function add(a, b) {
  return a + b;
}

function subtract(a, b) {
  return a - b;
}

module.exports = { add, subtract };

// app.js
const math = require('./math');

console.log(math.add(2, 3));      // 5
console.log(math.subtract(5, 2)); // 3
Exporting and requiring a small module

How require() Resolves a Path

When you call `require('./math')`, Node.js resolves it relative to the requiring file, tries `.js`, `.json`, and `.node` extensions in turn, and if the path is a directory, looks for a `package.json` `main` field or an `index.js`. A bare specifier with no leading `./` or `/`, like `require('lodash')`, instead walks up through `node_modules` folders starting from the current directory until it finds a match.

Modules Are Cached

The first time a module is required, Node.js executes it and stores the resulting `module.exports` in an internal cache keyed by the resolved file path. Every subsequent `require()` of that same file - from anywhere in the app - returns the cached object instead of re-running the file, which is why a module-level counter or a singleton database connection behaves the same across every file that requires it.

index.js
// counter.js
let count = 0;

module.exports = {
  increment() {
    count += 1;
    return count;
  },
};

// a.js
const counterA = require('./counter');
console.log(counterA.increment()); // 1

// b.js (required after a.js has already run)
const counterB = require('./counter');
console.log(counterB.increment()); // 2, not 1 - same cached instance
Module caching in action

Circular Dependency Pitfalls

If module A requires module B and module B requires module A back, Node.js breaks the cycle by handing out whatever partial `module.exports` A had built up so far, rather than looping forever. If B's `require('./a')` happens before A has finished assigning its exports, B receives an incomplete (possibly empty) object.

The practical fix is usually to restructure code so the shared piece lives in its own third module that both A and B require, or to require the dependency lazily inside a function body (so it resolves after both modules have fully loaded) instead of at the top of the file.

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

// b.js
console.log('b.js starting');
const a = require('./a');
console.log('in b.js, a.done =', a.done); // false - a.js has not finished yet
exports.message = 'hello from b';
A circular require and its symptom

Coding challenge

Create a stringUtils.js module that exports two functions, capitalize(str) and reverse(str), using module.exports, and a second file that requires it and prints both functions applied to "hello".

// Write your solution here

Answer every question below to mark this chapter as complete.

Quiz Yourself

Score: 0/5

1. What happens if you do `exports = { foo: 1 }` instead of `module.exports = { foo: 1 }`?

2. When you call require() on the same file twice from two different modules, what do you get?

3. How does require() resolve `require('lodash')` versus `require('./utils')`?

4. What is the underlying cause of a circular-dependency bug between two CommonJS modules?

5. Which arguments does Node.js pass into the function each CommonJS file is wrapped in?

You're viewing a free lesson. Sign up free to track your progress and unlock the full course.Sign up freeSign in