On this page

CommonJS vs ES Modules, Module Caching

As a Node.js app grows, you split code into modules: separate files that share code with each other. Node.js has two module systems, CommonJS and ES Modules, and both cache a module after it loads the first time.

Definition

  • CommonJS (CJS): Node's original module system. It uses require() to import and module.exports to export.
  • ES Modules (ESM): the official JavaScript standard, used in browsers and modern Node.js. It uses import and export.
  • Module caching: after a module is loaded once, Node.js stores it. Every later import returns the same stored copy, without running the file again.

Analogy: think of borrowing a book from a library. The first time, the library has to fetch it from storage (load and run the file). After that, it stays on the front shelf, and everyone who asks gets that same copy (the cache).

Syntax Side by Side

// math.js
function add(a, b) {
return a + b;
}
module.exports = { add };
// app.js
const { add } = require("./math");
console.log(add(2, 3)); // 5

Key Differences

CommonJSES Modules
Import / exportrequire() / module.exportsimport / export
LoadingSynchronous, at runtimeAsynchronous, analyzed before the code runs
Where imports can goAnywhere, even inside if or functionsTop level only (use import() for dynamic loading)
Exported valuesA copy of the value at export timeA live binding to the original variable
Top-level awaitNot supportedSupported
Strict modeOff by defaultAlways on
this at top levelmodule.exportsundefined
__dirname, __filenameAvailableNot available (use import.meta.dirname and import.meta.filename)
File extension in importsOptional (./math)Required (./math.mjs)
Tree shakingHard, because imports are dynamicEasy, because imports are static
Used in browsersNoYes

How Node.js Decides Which System to Use

FileTreated as
.cjsAlways CommonJS
.mjsAlways ES Module
.js with "type": "module" in package.jsonES Module
.js with no "type", or "type": "commonjs"CommonJS
{
"name": "my-app",
"type": "module"
}

Copy vs Live Binding

This difference is a common interview question.

// counter.js
let count = 0;
function increment() {
count++;
}
module.exports = { count, increment };
// app.js
const counter = require("./counter");
console.log(counter.count); // 0
counter.increment();
console.log(counter.count); // 0 (still the old copy)
  • In CommonJS, count was copied into the exports object when the module ran, so later changes don't show up.
  • In ES Modules, count is a live link to the variable inside counter.mjs, so you always see its current value.

Using One System From the Other

FromToHow
ESMCJSimport pkg from "./file.cjs" works. module.exports becomes the default export
CJSESMUse dynamic import(), which returns a promise. Newer Node.js versions (22.12+ and 20.19+) also allow require() of an ES module, as long as it doesn't use top-level await
// Inside a CommonJS file
async function main() {
const { add } = await import("./math.mjs");
console.log(add(2, 3));
}
main();

Module Caching

The first time a module is imported, Node.js runs the file and stores the result. Every later import gets the same object from the cache. The file does not run again.

// config.js
console.log("config loaded");
module.exports = { count: 0 };
// app.js
const a = require("./config"); // prints "config loaded"
const b = require("./config"); // nothing printed: comes from the cache
a.count++;
console.log(b.count); // 1
console.log(a === b); // true (the same object)

Step by step:

  1. The first require resolves the full path, runs config.js, and stores module.exports in the cache.
  2. The second require finds the path already in the cache and returns the same object.
  3. Changing a.count also changes b.count, because a and b are the same object.

ES Modules work the same way: each module is evaluated only once, and every import shares it.

Why Caching Is Useful

  • Faster: each file is read and run only once.
  • Shared state: a module works like a singleton. A database connection or config object is created once and shared across the app.
  • Safer circular imports: if two modules import each other, the cache stops them from loading forever.

The Cache Key Is the Resolved File Path

The cache is keyed by the full resolved path, not by the text you write in require.

ImportSame cache entry?
require("./config") and require("./config.js")Yes, both resolve to the same file
The same package installed in two different node_modules foldersNo, two different paths, so two separate copies

Clearing the Cache

// Remove the module from the cache, so the next require runs it again
delete require.cache[require.resolve("./config")];
const fresh = require("./config"); // prints "config loaded" again

You rarely need this outside of tests and hot-reloading tools.

Which Should You Use?

  • For new projects, use ES Modules: it is the standard, works in browsers, supports top-level await, and enables tree shaking.
  • Use CommonJS when working on older codebases or with packages that only support it.

Summary

  • CommonJS uses require / module.exports, loads synchronously, and allows imports anywhere.
  • ES Modules use import / export, are analyzed before running, and support top-level await.
  • CommonJS exports a copy. ES Modules export a live binding.
  • Node.js picks the system by file extension (.cjs, .mjs) or the "type" field in package.json.
  • ESM can import CJS directly. CJS loads ESM with import(), or with require() in newer Node.js versions.
  • Module caching: a module runs once, and every later import returns the same object.
  • The cache is keyed by the resolved file path. It can be cleared in CJS with require.cache.

Interview Answer: What Is the Difference Between CommonJS and ES Modules?

CommonJS is Node's original module system. It uses require() and module.exports, loads modules synchronously at runtime, and allows require anywhere in the code. ES Modules are the JavaScript standard. They use import and export, are analyzed before the code runs, support top-level await, run in strict mode, and work in browsers. Because ESM imports are static, bundlers can tree-shake unused code.

Another key difference is that CommonJS exports a copy of a value, while ES Modules export a live binding, so an importer always sees the variable's current value.

Both systems cache modules. The first import runs the file and stores the result, keyed by its resolved path. Every later import returns the same object without running the file again. That makes modules behave like singletons, which is useful for sharing a database connection or config. In CommonJS, I can clear a cached module by deleting it from require.cache.

Next TopicStreams, Piping, and Backpressure