Node.js: Node.js Module System
Last updated: 2026-08-26
Charlie’s project had ballooned from 3 files to 30. All the functions, configurations, and utility classes were crammed into one massive app.js, and changing a single function required half an hour of searching through 2,000 lines of code. He decided to break the code down into separate modules, only to discover that require and import looked different, module.exports and exports were always getting mixed up, and circular dependencies were causing the program to spit out a bunch of undefined. In this lesson, we’ll join Charlie as he unravels the Node.js module system, transforming his code from a tangled mess into a well-organized set of building blocks.
You'll learn:
- Organize code using CommonJS
require/module.exports/exports import,export, and"type":"module"configurations using ES Modules- Understand
require's module search mechanism (built-in → node_modules → path) - Understand the module caching mechanism and the role of
require.cache - Identify circular dependency issues and understand how Node.js handles them
1. CommonJS Modules
(1) Exporting with module.exports
Node.js uses the CommonJS module specification by default. Each file is a module that exports values using module.exports, and other files load them using require().
// math.js
function add(a, b) {
return a + b;
}
function subtract(a, b) {
return a - b;
}
module.exports = { add, subtract };
(2) Loading Modules with require
require() Accepts a module identifier and returns the module.exports value for that module.
// app.js
const math = require('./math');
console.log(math.add(10, 3)); // 13
console.log(math.subtract(10, 3)); // 7
(3) The exports shortcut
exports is a reference to module.exports and is suitable for adding properties one by one.
// logger.js
exports.info = function (msg) {
console.log(`[INFO] ${msg}`);
};
exports.error = function (msg) {
console.log(`[ERROR] ${msg}`);
};
▶ Example: Exporting a Single Function vs. Exporting an Object
// greet.js — Export a Single Function
module.exports = function (name) {
return `Hello, ${name}!`;
};
// config.js — Export Objects
module.exports = {
port: 3000,
host: 'localhost',
debug: true,
};
// app.js
const greet = require('./greet');
const config = require('./config');
console.log(greet('Charlie')); // Hello, Charlie!
console.log(`Server: ${config.host}:${config.port}`); // Server: localhost:3000
(4) module.exports vs exports Differences
| Feature | module.exports | exports |
|---|---|---|
| Essence | The module's actual exported object | Reference to module.exports |
| Assignment Export | ✅ module.exports = fn |
❌ exports = fn Remove Reference |
| Add one by one | ✅ module.exports.foo = fn |
✅ exports.foo = fn |
| Export a single value | ✅ Recommended | ❌ Not available |
| Security | Always valid | Expires after reassignment |
Core Principle: If you need to export a single function, class, or a brand-new object, you must use
module.exports;exportscan only be used to add properties.
2. ES Modules
(1) Basic Syntax
ES Modules (ESM) are the official JavaScript module standard, using the export and import syntax.
// utils.mjs
export function square(n) {
return n * n;
}
export const VERSION = '2.0.0';
export default function greet(name) {
return `Hello, ${name}!`;
}
// app.mjs
import greet, { square, VERSION } from './utils.mjs';
console.log(greet('Charlie')); // Hello, Charlie!
console.log(square(5)); // 25
console.log(VERSION); // 2.0.0
(2) Three Ways to Enable ESM
| Method | Description |
|---|---|
File extension .mjs |
Node.js automatically processes it as ESM |
"type": "module" in package.json |
Files named .js within the project default to ESM |
--input-type=module |
Command-line argument used for stdin input |
// package.json
{
"type": "module"
}
▶ Example: (3) Named Exports and Default Exports
// shapes.mjs
export const PI = 3.14159;
export function circleArea(radius) {
return PI * radius * radius;
}
export default class Shape {
constructor(name) {
this.name = name;
}
describe() {
return `This is a ${this.name}`;
}
}
▶ Example: Unified Export and Re-export
// api.mjs — Batch Export
export { addUser, removeUser } from './users.mjs';
export { logError } from './logger.mjs';
// You can also rename it
export { add as addUser } from './math.mjs';
3. Comparison of CommonJS and ESM
(1) Key Differences
| Dimension | CommonJS | ES Modules |
|---|---|---|
| Syntax | require() / module.exports |
import / export |
| Loading Method | Synchronous, runtime loading | Asynchronous, static analysis at compile time |
| Value Type | Value Copy (Primitive Types) | Value Binding (Live Reference) |
| Top-level this | module.exports |
undefined |
| Circular Dependencies | Returns Unresolved Exports | Reference Binding, but May Be in the TDZ |
| Use Case | Node.js Project (Default) | New Project, Code Sharing in the Browser |
| File extension | .js / .cjs |
.mjs / .js(type:module) |
▶ Example: (2) Value Copying vs. Binding
// counter.cjs — CommonJS
let count = 0;
function increment() {
count++;
}
module.exports = { count, increment };
// counter.mjs — ESM
export let count = 0;
export function increment() {
count++;
}
// CJS: count is a copy, it won't change
const c = require('./counter.cjs');
c.increment();
console.log(c.count); // 0 (still the initial value)
// ESM: count is bound, real-time updates
import { count, increment } from './counter.mjs';
increment();
console.log(count); // 1 (updated)
▶ Example: Importing a CJS module in ESM
// legacy.cjs
module.exports = { legacyMethod() { return 'old school'; } };
// app.mjs
import cjs from './legacy.cjs';
console.log(cjs.legacyMethod()); // old school
When
importa CJS module in ESM, the value ofmodule.exportsis used as the default export.
4. The require Module Lookup Mechanism
(1) Search Process
When you type require('express'), Node.js searches in the following order:
flowchart TD
A["require('express')"] --> B{Does it have a built-in module??}
B -- Yes --> C[Return built-in module]
B -- No --> D{Path starting with ./ or / ?}
D -- Yes --> E[Find file by path]
E --> E1[Try .js / .json / .node]
E1 --> E2[Try index.js]
D -- No --> F[Search node_modules]
F --> F1[Current Directory/node_modules/express]
F1 --> F2[Parent directory/node_modules/express]
F2 --> F3[Move up one level at a time until root directory]
F3 --> F4{Found?}
F4 -- No --> G[Throw MODULE_NOT_FOUND]
F4 -- Yes --> H[Load and cache module]
E2 --> H
C --> H
(2) Path Resolution Rules
| Required Parameter | Parsing Method | Example |
|---|---|---|
./math |
Path relative to the current file | ./math → /project/src/math.js |
../utils |
Relative to parent directory | ../utils → /project/utils.js |
/abs/path |
Absolute Path | /lib/helper.js |
express |
Built-in modules → node_modules | Search by level |
| Scope package |
▶ Example: Viewing the module resolution path
// show-paths.js
console.log(module.paths);
[
'/project/src/node_modules',
'/project/node_modules',
'/node_modules',
'C:\\Users\\Charlie\\.node_modules',
'C:\\Users\\Charlie\\.node_libraries',
'C:\\Program Files\\nodejs\\lib\\node'
]
5. Module Caching Mechanism
(1) How Caching Works
require When a module is loaded for the first time, its code is executed and the result is cached. Subsequently, require the same module returns the cached result directly without re-executing the code.
// counter.js
console.log('counter.js executed!');
let count = 0;
module.exports = {
increment() { return ++count; },
getCount() { return count; },
};
// app.js
const c1 = require('./counter'); // counter.js executed!
const c2 = require('./counter'); // (no output, using cache)
console.log(c1 === c2); // true
console.log(c1.increment()); // 1
console.log(c2.getCount()); // 1 (shared state)
(2) require.cache
All loaded modules are cached in the require.cache object, with the absolute path of the module serving as the key.
// inspect-cache.js
const path = require('path');
const math = require('./math');
const cacheKey = path.resolve(__dirname, 'math.js');
console.log(require.cache[cacheKey] !== undefined); // true
console.log(require.cache[cacheKey].exports === math); // true
▶ Example: Clearing the Cache to Implement Hot Reloading
// hot-reload.js
function loadConfig() {
const path = require('path');
const cacheKey = path.resolve(__dirname, 'config.js');
delete require.cache[cacheKey];
return require('./config');
}
const cfg1 = loadConfig();
// ... config.js Modified ...
const cfg2 = loadConfig(); // Re-execute, loading the latest content
If you delete the entry in
require.cacheand then runrequireagain, Node.js will re-execute that module. This is useful for hot reloading in a development environment, but use it with caution in a production environment.
6. Overview of Built-in Modules
(1) Quick Reference for Common Built-in Modules
Node.js comes with a large number of built-in modules that are ready to use without installation.
| Module | Purpose | Common Methods/Properties |
|---|---|---|
fs |
File system operations | readFile, writeFile, readdir, stat |
path Path processing join, resolve, parse, extname, basename |
||
http |
HTTP Server/Client | createServer, get, request |
https |
HTTPS Server/Client | createServer, get, request |
url URL parsing and construction URL, fileURLToPath, pathToFileURL |
||
os |
Operating System Information | cpus, freemem, hostname, platform |
events |
Event Emitter | EventEmitter, on, emit, off |
stream |
Stream processing | Readable, Writable, Transform, pipe |
crypto |
Encryption and Hash | createHash, createHmac, randomBytes |
util |
Utility | promisify, callbackify, format, inspect |
child_process |
Child Process Management | exec, spawn, fork |
buffer |
Binary Data Processing | Buffer.alloc, Buffer.from, concat |
▶ Example: (2) Built-in modules require no installation
const fs = require('fs');
const path = require('path');
const os = require('os');
console.log(os.platform()); // win32 / darwin / linux
console.log(path.join('/project', 'src', 'app.js')); // /project/src/app.js
▶ Example: Quick Start with path and os
const path = require('path');
const os = require('os');
const filePath = '/project/src/utils/helper.js';
console.log(path.extname(filePath)); // .js
console.log(path.dirname(filePath)); // /project/src/utils
console.log(path.basename(filePath)); // helper.js
console.log(`CPU cores: ${os.cpus().length}`);
console.log(`Free memory: ${(os.freemem() / 1024 / 1024).toFixed(0)} MB`);
7. Circular Dependencies
(1) What Is a Circular Dependency?
Module A requires Module B, and Module B requires Module A, creating a circular reference. Node.js does not enter an infinite loop; instead, it returns the exports from the portion that has already been executed.
▶ Example: (2) How Node.js Handles It
// a.js
exports.loaded = false;
const b = require('./b');
exports.loaded = true;
console.log('a.js - b.loaded =', b.loaded);
// b.js
exports.loaded = false;
const a = require('./a'); // Received a Unfinished exports { loaded: false }
exports.loaded = true;
console.log('b.js - a.loaded =', a.loaded);
node a.js
b.js - a.loaded = false
a.js - b.loaded = true
When b.js executes require('./a'), a.js has not yet finished executing, so Node.js returns the portion of a.js that has been assigned a value at that point ({ loaded: false }).
(3) Strategies for Avoiding Circular Dependencies
| Strategy | Description |
|---|---|
| Extract shared logic | Move the common parts to a third module |
| Deferred require | Move require inside the function so it loads only when called |
| Event Decoupling | Use EventEmitter Instead of Direct Calls |
| Dependency Injection | Passing dependencies via parameters rather than hard-coding them |
▶ Example: Delaying require to Resolve Circular Dependencies
// user.js
exports.getName = function () {
return 'Charlie';
};
exports.getProfile = function () {
const format = require('./format'); // Delay until the time of the call require
return format.upper(exports.getName());
};
// format.js
exports.upper = function (str) {
return str.toUpperCase();
};
exports.getUserDisplay = function () {
const user = require('./user'); // Deferred require
return `User: ${user.getName()}`;
};
Deferring
requireensures that the module loads its dependencies only when a method is called for the first time, by which point both modules have been fully initialized, thereby preventing the retrieval of incompleteexports.
8. Comprehensive Example: Modular Project
Below, we'll create a modular project that includes a utility module, a logging module, and a main entry point:
// math.js — Tools Module
const PI = 3.14159;
function circleArea(radius) {
return PI * radius * radius;
}
function rectangleArea(width, height) {
return width * height;
}
function round(value, decimals = 2) {
const factor = Math.pow(10, decimals);
return Math.round(value * factor) / factor;
}
module.exports = { circleArea, rectangleArea, round };
// logger.js — Log Module
const LEVELS = { INFO: 'INFO', WARN: 'WARN', ERROR: 'ERROR' };
function formatMessage(level, msg) {
const timestamp = new Date().toISOString();
return `[${timestamp}] [${level}] ${msg}`;
}
function info(msg) {
console.log(formatMessage(LEVELS.INFO, msg));
}
function warn(msg) {
console.warn(formatMessage(LEVELS.WARN, msg));
}
function error(msg) {
console.error(formatMessage(LEVELS.ERROR, msg));
}
module.exports = { info, warn, error, LEVELS };
// app.js — Main Entrance
const { circleArea, rectangleArea, round } = require('./math');
const { info, error } = require('./logger');
const radius = 5;
const area = round(circleArea(radius));
info(`Circle area (r=${radius}): ${area}`);
const roomArea = rectangleArea(4.5, 6.2);
info(`Room area: ${round(roomArea)} sqm`);
if (radius < 0) {
error('Radius cannot be negative');
} else {
info('Calculation complete');
}
node app.js
[2026-07-03T10:30:00.000Z] [INFO] Circle area (r=5): 78.54
[2026-07-03T10:30:00.001Z] [INFO] Room area: 27.9 sqm
[2026-07-03T10:30:00.001Z] [INFO] Calculation complete
❓ FAQ
module.exports as the default export), but in CJS, you cannot use require to load ESM modules; you must use the dynamic import() function instead. It is recommended that projects adhere to a single module specification.require synchronous or asynchronous?require blocks code execution until the module has finished loading. This is why Node.js recommends placing require at the top of a file and avoiding frequent require calls for new modules in the hot path during runtime.module.exports and exports?exports is a shorthand reference to module.exports. You can add properties using exports.xxx = ..., but exports = xxx breaks the reference, causing the export to fail. When you need to export a single function or a brand-new object, you must use module.exports = xxx.require.cache object. The keys are the module's absolute paths, and the values are the module objects. If you delete a key (delete require.cache[key]), the module will be re-executed the next time you use require.exports that have not yet been fully executed at the point where the loop begins (which may be incomplete objects), potentially resulting in undefined properties. Solutions include extracting a common module, deferring require calls, and decoupling events.import statements be written at the top level in ESM?import() function.require to load a JSON file?JSON.parse(), returning a parsed JavaScript object. This is commonly used for loading configuration files.📖 Summary
- CommonJS is the default module specification for Node.js; use
requireto load andmodule.exportsto export - ES Modules are an official JavaScript standard; use
import/export, and enable them with the.mjsor"type":"module"suffix requireSearch order: Built-in modules → Relative/absolute paths → node_modules, working upward level by level- After the module is loaded for the first time, it is cached in
require.cache; subsequentrequirecalls return the cached version directly. exportsis a reference tomodule.exports; reassigning it will break the reference- When a circular dependency occurs, Node.js returns unfulfilled
exports; this can be avoided using strategies such as delayingrequirecalls. - Built-in modules (such as fs, path, http, os, etc.) do not require installation; simply use
requireto use them.
📝 Exercises
- Create the
calculator.jsmodule, export the four functionsadd,subtract,multiply, anddivide, then require and use them inmain.js - Convert the previous question to an ESM version: Use the
exportsyntax and the.mjssuffix, and load it usingimport. - Write code to verify the existence of
require.cache: After requiring a module, print the information about that module fromrequire.cache. - Intentionally create a circular dependency (a.js requires b.js, and b.js requires a.js), observe the output, and then fix it by using deferred requires.
- Use the
pathandosmodules to print the current operating system platform, the number of CPU cores, and the absolute path of the current directory.