NodeJSCommonJS Modules (require)

CommonJS Modules (require)

CommonJS (CJS) is Node's original module system, and it still powers the majority of packages on npm. It is built on two primitives: require() to pull a module in, and module.exports to expose values out. Its defining characteristic is that it is synchronousrequire blocks until the target module is fully loaded and evaluated.

Importing with require

JS
const fs = require('node:fs')          // core module
const express = require('express')      // third-party (node_modules)
const utils = require('./utils')        // local file (extension optional)
const { join } = require('node:path')   // destructure a specific export

require returns whatever the target module assigned to its module.exports. You can grab the whole object or destructure the pieces you need.

Exporting with module.exports

logger.js

JS
function info(msg) { console.log('[INFO]', msg) }
function error(msg) { console.error('[ERROR]', msg) }

module.exports = { info, error }   // export an object of functions

Single-value export — a class or function

JS
class Logger { /* ... */ }
module.exports = Logger   // the whole module IS the class
exports vs module.exports
`exports` starts as a *reference* to the same object as `module.exports`. `exports.foo = 1` works, but `exports = {...}` **breaks the link** and exports nothing — only `module.exports` is actually returned. This subtlety has its own page: [module.exports & exports](/nodejs/module-exports).
require is synchronous — and that matters

When you call require('./big'), Node stops, reads the file from disk, evaluates it top to bottom, and only then returns. Because module loading happens once at startup, this blocking is acceptable — but it shapes how CJS behaves:

  • A module body runs once, the first time it is required (then cached — see Module Caching).

  • You can require conditionally, inside a function, or compute the path at runtime — it is just a function call.

  • Blocking I/O at module scope (a sync file read) freezes startup until it finishes.

JS
// Legal in CJS — require is an ordinary function, evaluated when reached
if (process.env.NODE_ENV === 'development') {
  const devTools = require('./dev-tools')
  devTools.enable()
}
What runs when you require a file

require('./math') the first time

Text
1. Resolve './math' to an absolute file path (see require-resolution)
2. Is it already in the module cache?  →  yes: return cached exports, done
3. Create module object; insert into cache (BEFORE running — enables cycles)
4. Wrap the file source in the module wrapper function
5. Execute the wrapped code, populating module.exports
6. Return module.exports to the caller

Steps 1 and 4 each have a dedicated page: How require() Resolves Modules and The Module Wrapper Function.

Circular dependencies

If a.js requires b.js while b.js requires a.js, you have a cycle. Because Node caches the module object before running it (step 3), the second require returns a partially populated exports — whatever had been assigned so far:

a.js

JS
exports.done = false
const b = require('./b')          // runs b.js now
console.log('in a, b.done =', b.done)
exports.done = true

b.js

JS
const a = require('./a')          // a is only partially done here!
console.log('in b, a.done =', a.done)   // false — a hasn't finished
exports.done = true
in b, a.done = false
in a, b.done = true
Cycles are a design smell
CommonJS tolerates cycles but hands you incomplete objects, causing baffling `undefined` bugs. The real fix is to break the cycle: extract the shared piece into a third module both depend on, or defer the `require` to inside the function that needs it.
require.main — "am I the entry point?"

A common pattern lets a file act as both an importable module and a runnable script:

JS
function main() { /* ... */ }

if (require.main === module) {
  main()        // executed only when run directly: node thisfile.js
}
module.exports = { main }   // still importable elsewhere
Next
Master the export surface in [module.exports & exports](/nodejs/module-exports).