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 synchronous — require blocks until the target module is fully loaded and evaluated.
Importing with require
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 exportrequire 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
function info(msg) { console.log('[INFO]', msg) }
function error(msg) { console.error('[ERROR]', msg) }
module.exports = { info, error } // export an object of functionsSingle-value export — a class or function
class Logger { /* ... */ }
module.exports = Logger // the whole module IS the classrequire 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
requireconditionally, 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.
// 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
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
exports.done = false
const b = require('./b') // runs b.js now
console.log('in a, b.done =', b.done)
exports.done = trueb.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 = truein b, a.done = false in a, b.done = true
require.main — "am I the entry point?"
A common pattern lets a file act as both an importable module and a runnable script:
function main() { /* ... */ }
if (require.main === module) {
main() // executed only when run directly: node thisfile.js
}
module.exports = { main } // still importable elsewhere