Skip to content
Node.js nd modules 4 min read

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 use module.exports = ... whenever you want to replace the entire exported value (for example, exporting a single function or class). When in doubt, reach for module.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:

VariablePurpose
exportsShortcut reference to module.exports.
requireFunction to import other modules.
moduleMetadata object holding exports, id, and the cache entry.
__filenameAbsolute path of the current file.
__dirnameAbsolute path of the directory containing the file.

Note: __filename and __dirname exist only in CommonJS. In ES modules you must derive them from import.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 bare exports.foo shorthand for attaching multiple named members.
  • Never reassign exports directly — it silently detaches from module.exports and exports nothing.
  • Always prefix local module paths with ./ or ../ so Node doesn’t mistake them for core or node_modules packages.
  • 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.
Last updated June 14, 2026
Was this helpful?