Historical Context & Motivation
When JavaScript was first created in 1995, it was designed to run inside web browsers—short scripts that made buttons clickable or validated a form. Every script shared one big, global space, which meant that if two scripts used the same variable name, they could overwrite each other. As programs grew larger, this became a real headache. Developers needed a way to split code into modules—self-contained files that could share only what they chose to share.
In 2009, a project called Node.js brought JavaScript to the server side—meaning you could run JavaScript outside of a browser, on your own computer or a web server. Node.js needed a module system right away, because server programs are often large and complex. The solution was CommonJS, a standard that introduced two key tools: require() to load a module, and module.exports to make parts of a module available to other files.
require() and module.exports.import and export. CommonJS remains the default in Node.js for years afterward.The central question CommonJS answers is: How do you break a large JavaScript program into smaller, reusable files that can safely share code without polluting a single global namespace?
Core Principles & Definitions
CommonJS revolves around a small set of ideas that keep your code organized and predictable. Every file in a Node.js project is treated as its own module. The variables and functions inside a file are private by default—nothing leaks out unless you explicitly choose to export it. Let's break down the foundational concepts.
Module = File
.js file is automatically a separate module with its own private scope. Variables declared inside one file cannot be accessed by another file unless explicitly exported.module.exports
module.exports. Whatever you assign to this object becomes the module's public interface—the stuff other files can use.require()
require() function loads another module by file path. It returns whatever that module placed on module.exports, giving you access to its public code.Synchronous Loading
require() call, it pauses, reads the file, runs it, and then continues with the next line.Caching
require() the same file from multiple places, the code inside only runs once. Every caller gets the same cached exports object.module.exports is like labeling certain tools and placing them on a shelf where others can grab them. require() is like walking over to that shelf and picking up what you need. You only get the tools the owner chose to share—everything else stays locked away.Visual Explanation — How require and module.exports Connect Files
math.js keeps PI and square private (gray text) while exporting areaOfCircle (green text). On the right, app.js uses require('./math') (yellow arrow) and receives only the exported object (green arrow). Trying to access PI or square from app.js returns undefined.Notice the key idea in the diagram: the public/private boundary is controlled entirely by what you place on module.exports. The variable PI and the helper function square are used internally to compute the area, but they never leave math.js. This keeps your code clean, prevents accidental conflicts, and makes it clear exactly what each module is responsible for.
How CommonJS Works Under the Hood
When you call require('./math'), Node.js performs several steps behind the scenes. Understanding this process helps you predict how your modules behave and debug problems when something goes wrong.
The Five-Step require() Process
- Resolve — Node.js figures out the absolute file path. If you write
require('./math'), it looks formath.jsin the same directory. If you writerequire('lodash')(no dot), it looks in thenode_modulesfolder. - Check Cache — Node.js checks if this module was already loaded. If so, it returns the cached
module.exportsimmediately without re-running the file. - Wrap — Node.js wraps the file's code in a special function that provides
require,module,exports,__dirname, and__filenameas local variables. This wrapper is what creates each module's private scope. - Execute — The wrapped code runs from top to bottom. Any assignments to
module.exportshappen during this step. - Return — Node.js returns the value of
module.exportsto the calling file.
The Module Wrapper Function
Step 3 is the secret sauce. Before your code ever runs, Node.js wraps it like this:
exports is a shortcut reference to module.exports. require is the function to load other modules. __filename and __dirname hold the current file's full path and directory. Because your code is inside a function, your variables are local—not global.exports is just a convenience reference that points to module.exports. You can add properties to it like exports.myFunc = ..., but if you reassign exports itself (e.g., exports = { ... }), you break the link and nothing gets exported. When in doubt, always use module.exports.Common Export Patterns
There are several ways to structure your exports in CommonJS. Each pattern suits a different situation. Let's look at the three most common approaches you'll encounter in real Node.js projects.
The most important thing to remember is that module.exports can hold any JavaScript value—an object, a function, a class, a string, or even a number. Whatever you assign to it is exactly what the consumer receives when they call require(). In practice, most modules export either an object (Pattern 1) or a single function (Pattern 2).
Worked Example — Building a Temperature Converter Module
Let's walk through creating a module from scratch and using it in another file. We'll build a small temperature converter that converts between Fahrenheit and Celsius.
tempConverter.js. Inside, define two functions: one to convert Fahrenheit to Celsius, and one to go the other direction. These functions use the standard formulas.function toCelsius(f) { return (f - 32) * 5 / 9; } and function toFahrenheit(c) { return (c * 9 / 5) + 32; }module.exports with both functions as properties.module.exports = { toCelsius, toFahrenheit };app.js (in the same folder), load the module using require(). The dot-slash tells Node.js to look in the current directory.const temp = require('./tempConverter');temp is the object we exported. We can call temp.toCelsius(212) to convert boiling water's temperature, and temp.toFahrenheit(0) to convert freezing point.temp.toCelsius(212) → 100 and temp.toFahrenheit(0) → 32node app.js. Node.js resolves the require path, loads tempConverter.js, executes it, caches the result, and returns the exports object. Your conversions print to the console.100 and 32const temp = require('./tempConverter'), you can destructure directly: const { toCelsius, toFahrenheit } = require('./tempConverter');. Now you can call toCelsius(212) without the temp. prefix. This is a common pattern in real projects.Strengths & Limitations of CommonJS
CommonJS was a groundbreaking solution when it was introduced, and it remains extremely useful in Node.js. However, like any tool, it has trade-offs. Understanding these strengths and limitations helps you know when CommonJS is the right choice—and when you might want an alternative.
| Aspect | Strength ✓ | Limitation ✗ |
|---|---|---|
| Simplicity | Easy to learn. Just two tools: require() and module.exports. | No advanced features like named or default exports in one statement. |
| Loading | Synchronous loading is predictable—code runs in the order you write it. | Synchronous loading blocks execution. Not ideal for browsers where you want to load files in parallel. |
| Ecosystem | The npm registry has over 2 million CommonJS packages. Massive community support. | Newer packages are increasingly published as ES Modules only, requiring extra configuration. |
| Caching | Modules are cached after first load, improving performance in large apps. | Cached module state can cause unexpected behavior if a module stores mutable data. |
| Tree Shaking | N/A—not a strength area. | Bundlers cannot easily remove unused code (tree-shake) from CommonJS modules because exports are dynamic. |
CommonJS vs. ES Modules — Looking Ahead
As you grow as a JavaScript developer, you'll encounter ES Modules (ESM), the newer module system built directly into the JavaScript language standard. Knowing how CommonJS compares to ESM helps you navigate modern codebases where both systems coexist.
| Feature | CommonJS (CJS) | ES Modules (ESM) |
|---|---|---|
| Syntax | require() / module.exports | import / export |
| Loading | Synchronous (one at a time) | Asynchronous (can load in parallel) |
| Where it runs | Node.js (server-side) | Browsers and Node.js |
| Dynamic require | Yes — require() can appear inside if-statements | Static import must be at the top; dynamic import() returns a promise |
| Tree Shaking | Not supported well | Fully supported — bundlers remove unused exports |
| File extension | .js (default in Node.js) | .mjs or .js with "type": "module" in package.json |
You don't need to choose one system and ignore the other. In practice, many Node.js projects still use CommonJS, especially older ones and those that rely on packages from npm. Newer front-end projects tend to use ES Modules. Learning CommonJS first gives you a solid foundation because the core concept of encapsulation and explicit sharing is identical in both systems. Once you're comfortable with require and module.exports, switching to import and export will feel natural.
Practice Problems
utils.js containing: const SECRET = 42; function double(n) { return n * 2; } module.exports = { double }; In app.js, you write const utils = require('./utils');. What is the value of utils.double(5)? What is the value of utils.SECRET?greet.js: exports = function(name) { return 'Hello ' + name; }; Then in app.js: const greet = require('./greet'); console.log(greet('Sam')); This throws an error: greet is not a function. Explain why, and fix the bug.questions.js (stores an array of question objects), scorer.js (has a function that takes an array of answers and returns a score), and app.js (the main file). Write the module.exports line for each module, and write the require() lines in app.js.counter.js exports an object: let count = 0; module.exports = { increment() { count++; }, getCount() { return count; } }; Two different files both require('./counter'). File A calls counter.increment() three times. Then File B calls counter.getCount(). What value does File B see? Explain why, referencing how CommonJS caching works.Lesson Summary
CommonJS is the module system that powers Node.js, turning each .js file into its own private module. You use module.exports to make functions, objects, or classes available to other files, and require() to load those exports into a consumer file. Everything not explicitly exported stays private, thanks to the module wrapper function that Node.js applies behind the scenes.
The three common export patterns are: exporting an object with multiple functions, exporting a single function, or exporting a class. CommonJS loads modules synchronously and caches them after first load, so the same module is never executed twice. While ES Modules (import/export) are the modern standard, understanding CommonJS is essential because millions of existing packages depend on it. The core lesson—encapsulate your code and share only what's needed—applies to every module system you'll ever use.