CommonJS Modules (require & module.exports)
CommonJS (CJS) is the module system that Node.js shipped with from the very beginning, and it remains the default for any file with a .js extension in a package that doesn’t opt into ES modules. It is built around three building blocks: the require() function to import, the module.exports object to export, and a per-file scope that keeps your variables private. Understanding CommonJS is essential because the vast majority of packages on npm still ship CJS, and you will encounter it constantly even as ES modules become the modern default.
How require and module.exports work
In CommonJS, every file is a module with its own scope. Whatever you assign to module.exports becomes the public value returned when another file calls require() on it. Anything you don’t export stays private to that file.
// math.js
function add(a, b) {
return a + b;
}
function subtract(a, b) {
return a - b;
}
// Export an object exposing both functions.
module.exports = { add, subtract };
// app.js
const math = require('./math');
console.log(math.add(2, 3));
console.log(math.subtract(10, 4));
Output:
5
6
The require('./math') call is synchronous: Node resolves the path, loads the file, executes it, and returns its module.exports value all in one blocking step. Note the relative path prefix ./ — without it, Node would treat math as a core module or a package inside node_modules.
The exports shortcut
For convenience, Node gives every module a local variable called exports that initially points to the same object as module.exports. You can attach properties to it as a shorthand.
// logger.js
exports.info = (msg) => console.log(`[INFO] ${msg}`);
exports.error = (msg) => console.error(`[ERROR] ${msg}`);
This works because exports and module.exports reference the same object, so adding properties to one affects the other. However, the relationship breaks the moment you reassign exports entirely:
// BROKEN: reassigning exports detaches it from module.exports
exports = { info: () => {} }; // require() still returns the ORIGINAL object
Rule of thumb: use
exports.foo = ...to attach individual members, but usemodule.exports = ...whenever you want to replace the entire exported value (for example, exporting a single function or class). When in doubt, reach formodule.exports.
Exporting a function, object, or class
The exported value can be anything. Here are the three most common shapes.
Exporting a single function as the whole module:
// greet.js
module.exports = function greet(name) {
return `Hello, ${name}!`;
};
const greet = require('./greet');
console.log(greet('Ada'));
Exporting an object namespace of related helpers:
// config.js
module.exports = {
port: 3000,
host: '127.0.0.1',
env: process.env.NODE_ENV ?? 'development',
};
Exporting a class:
// queue.js
class Queue {
#items = [];
enqueue(item) {
this.#items.push(item);
return this;
}
dequeue() {
return this.#items.shift();
}
get size() {
return this.#items.length;
}
}
module.exports = Queue;
const Queue = require('./queue');
const q = new Queue();
q.enqueue('a').enqueue('b');
console.log(q.dequeue(), q.size);
Output:
a 1
Module caching
The first time a module is required, Node executes it and stores the resulting module.exports in an internal cache keyed by the resolved file path. Every subsequent require() of the same file returns the cached value without re-running the file. This makes modules effective singletons.
// counter.js
let count = 0;
module.exports = {
increment: () => ++count,
value: () => count,
};
// app.js
const a = require('./counter');
const b = require('./counter'); // same cached instance as `a`
a.increment();
b.increment();
console.log(a.value()); // shared state
console.log(a === b); // same object reference
Output:
2
true
You can inspect or clear the cache through require.cache, though deleting entries is rarely needed outside of testing or hot-reloading scenarios.
delete require.cache[require.resolve('./counter')];
The module wrapper function
Before executing a module’s code, Node wraps it in a function so that top-level variables don’t leak into the global scope. Conceptually, your file is compiled as:
(function (exports, require, module, __filename, __dirname) {
// your module code lives here
});
This wrapper is why those five identifiers are always available inside a CommonJS file even though you never declared them. Their roles:
| Variable | Purpose |
|---|---|
exports | Shortcut reference to module.exports. |
require | Function to import other modules. |
module | Metadata object holding exports, id, and the cache entry. |
__filename | Absolute path of the current file. |
__dirname | Absolute path of the directory containing the file. |
Note:
__filenameand__dirnameexist only in CommonJS. In ES modules you must derive them fromimport.meta.url, which is a common reason code breaks when migrating between the two systems.
Best practices
- Prefer
module.exports = ...for clarity, and reserve the bareexports.fooshorthand for attaching multiple named members. - Never reassign
exportsdirectly — it silently detaches frommodule.exportsand exports nothing. - Always prefix local module paths with
./or../so Node doesn’t mistake them for core ornode_modulespackages. - Treat modules as singletons: remember that shared mutable state in a module persists across every
require()thanks to caching. - Keep modules small and focused on a single responsibility so their exported surface stays easy to reason about.
- For new projects, prefer ES modules (
import/export); use CommonJS mainly when interoperating with existing CJS code or tooling that still requires it.