Skip to content
Moduleshigh

CommonJS Modules

CommonJS (CJS) is Node.js's original module system. Files use require() to import and module.exports or exports to export. All require() calls are synchronous.

Memory anchor

CommonJS = sending a package via snail mail. You pack it up (module.exports), it ships synchronously, and the recipient gets a snapshot (copy). 'exports' is just a sticky note on the box—rip it off (reassign) and the box ships empty.

Expected depth

CJS modules are loaded synchronously: require() blocks until the module is parsed, executed, and its exports are returned. Modules are cached after first load—subsequent require() calls return the cached exports object. This caching means module-level state is shared across all importers. The exports shorthand is a reference to module.exports; reassigning exports breaks the link (exports = {} does nothing useful; module.exports = {} works).

Deep — senior internals

The CJS module wrapper function is how Node provides module-level scope: `(function(exports, require, module, __filename, __dirname) { /* module code */ })`. This is why __filename and __dirname are available without explicit import. The require() function resolves modules through an algorithm: exact file path → .js/.json/.node extension → index.js/index.json/index.node → node_modules lookup up the directory tree. Loaded module source is cached at require.cache[filename]; deleting an entry from require.cache causes the next require() to re-execute the module. This technique is used in hot-reload implementations but can cause memory leaks if not managed carefully.

🎤Interview-ready answer

CommonJS loads modules synchronously via require(), wrapping each module in a function that provides exports, require, module, __filename, __dirname. Modules are cached post-load, making module-level singletons reliable. The critical behavior: reassigning exports doesn't affect module.exports—only mutations to the exports object or direct assignment to module.exports work for export.

Common trap

exports.foo = bar works because it mutates the object that module.exports points to. But exports = { foo: bar } reassigns the local variable, breaking the reference. The module still exports whatever module.exports holds (the original empty object). This silently exports nothing and is a classic footgun.

Related concepts