NodeJSmodule.exports & exports

module.exports & exports

Every CommonJS module has a module object, and module.exports is what require() hands back to whoever imports it. There is also a second name, exports, that confuses nearly everyone at first. This page makes the relationship precise so you never accidentally export nothing again.

The mental model

At the very start of every module, Node effectively runs this line for you:

JS
// Conceptually, Node does this before your code runs:
var exports = module.exports = {}

So exports and module.exports both point at the same empty object. require only ever returns module.exportsexports is just a convenient shorthand reference to it. Everything that follows is a consequence of that one fact.

Why exports = {...} silently fails
Reassigning `exports` breaks the link
`exports.foo = 1` mutates the shared object, so it works. But `exports = { foo: 1 }` just repoints the *local variable* `exports` at a new object — `module.exports` still points at the original empty one, and that empty object is what gets returned. The result: your module exports nothing.

JS
// WORKS — mutates the shared object
exports.add = (a, b) => a + b

// BROKEN — reassigns the local, module.exports is untouched
exports = { add: (a, b) => a + b }   // require() returns {} !

// WORKS — assign to module.exports directly
module.exports = { add: (a, b) => a + b }
The two export styles

Goal

Write

Import as

A bag of named things

module.exports = { a, b }

const { a, b } = require(...)

One single thing

module.exports = MyClass

const MyClass = require(...)

Add one named export

exports.a = ...

const { a } = require(...)

Named exports — a utility collection

JS
// strings.js
exports.upper = (s) => s.toUpperCase()
exports.lower = (s) => s.toLowerCase()
// → require('./strings') gives { upper, lower }

Single export — the module IS one value

JS
// User.js
class User { /* ... */ }
module.exports = User
// → const User = require('./User')
Common rule of thumb
Pick one and be consistent
If a module's job is *one* thing (a class, a single function, a configured client), use `module.exports = thing`. If it is a *collection* of helpers, attach them with `exports.name = ...` or assign one object literal at the end. Mixing both in one file is what leads to "why is half my API undefined?".
Do not mix module.exports = X with exports.y =
After `module.exports = MyClass`, any `exports.helper = ...` is **lost** — `exports` still points at the old object, but `require` now returns `MyClass`. To attach extras, hang them off the same value: `MyClass.helper = ...` or `module.exports.helper = ...`.
Exports are live references, not copies

Because require returns the actual module.exports object, mutations made after export are visible to importers (the object is shared). But rebinding a variable inside the module does not propagate, because the importer captured the value, not the binding:

counter.js

JS
let count = 0
exports.count = count
exports.increment = () => { count++; exports.count = count }

JS
const c = require('./counter')
console.log(c.count)   // 0
c.increment()
console.log(c.count)   // 1 — because increment reassigned exports.count
This is where ESM differs
ES Modules export *live bindings*: an importer always sees the current value of an exported `let`, even after the module reassigns it — no manual `exports.count = count` needed. CommonJS exports values. See [CommonJS vs ES Modules](/nodejs/commonjs-vs-esm).
Exporting a function with attached properties

A handy pattern — export a primary function that also carries helpers, exactly how many libraries (like Express) ship:

JS
function createApp(opts) { /* ... */ }
createApp.version = '1.0.0'
createApp.defaults = { port: 3000 }

module.exports = createApp
// → const createApp = require('./app'); createApp.version
Next
See exactly how Node turns a string like './utils' into a file on disk in [How require() Resolves Modules](/nodejs/require-resolution).