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:


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().

JAVASCRIPT
// 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.

JAVASCRIPT
// 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.

JAVASCRIPT
// 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

JAVASCRIPT
// 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,
};
▶ Try it Yourself
JAVASCRIPT
// 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; exports can 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.

JAVASCRIPT
// utils.mjs
export function square(n) {
  return n * n;
}

export const VERSION = '2.0.0';

export default function greet(name) {
  return `Hello, ${name}!`;
}
JAVASCRIPT
// 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
JSON
// package.json
{
  "type": "module"
}

▶ Example: (3) Named Exports and Default Exports

JAVASCRIPT
// 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}`;
  }
}
▶ Try it Yourself

▶ Example: Unified Export and Re-export

JAVASCRIPT
// 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';
▶ Try it Yourself

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

JAVASCRIPT
// counter.cjs — CommonJS
let count = 0;
function increment() {
  count++;
}
module.exports = { count, increment };
▶ Try it Yourself
JAVASCRIPT
// counter.mjs — ESM
export let count = 0;
export function increment() {
  count++;
}
JAVASCRIPT
// 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)
JAVASCRIPT
// 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

JAVASCRIPT
// legacy.cjs
module.exports = { legacyMethod() { return 'old school'; } };
▶ Try it Yourself
JAVASCRIPT
// app.mjs
import cjs from './legacy.cjs';
console.log(cjs.legacyMethod()); // old school

When import a CJS module in ESM, the value of module.exports is 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:

100%
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

JAVASCRIPT
// show-paths.js
console.log(module.paths);
▶ Try it Yourself
TEXT 📖 Display only
[
  '/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.

JAVASCRIPT
// counter.js
console.log('counter.js executed!');
let count = 0;
module.exports = {
  increment() { return ++count; },
  getCount() { return count; },
};
JAVASCRIPT
// 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.

JAVASCRIPT
// 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

JAVASCRIPT
// 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
▶ Try it Yourself

If you delete the entry in require.cache and then run require again, 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

JAVASCRIPT
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
▶ Try it Yourself

▶ Example: Quick Start with path and os

JAVASCRIPT
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`);
▶ Try it Yourself

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

JAVASCRIPT
// a.js
exports.loaded = false;
const b = require('./b');
exports.loaded = true;
console.log('a.js - b.loaded =', b.loaded);
▶ Try it Yourself
JAVASCRIPT
// 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);
BASH
node a.js
TEXT 📖 Display only
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

JAVASCRIPT
// 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());
};
▶ Try it Yourself
JAVASCRIPT
// format.js
exports.upper = function (str) {
  return str.toUpperCase();
};

exports.getUserDisplay = function () {
  const user = require('./user'); // Deferred require
  return `User: ${user.getName()}`;
};

Deferring require ensures 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 incomplete exports.



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:

JAVASCRIPT
// 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 };
JAVASCRIPT
// 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 };
JAVASCRIPT
// 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');
}
BASH
node app.js
TEXT 📖 Display only
[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

Q Can CommonJS and ESM be used together?
A There are limitations. In ESM, you can import CJS modules (with 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.
Q Is require synchronous or asynchronous?
A Synchronous. 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.
Q What is the difference between module.exports and exports?
A 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.
Q How do I view a module's cache?
A You can view it using the 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.
Q What is a circular dependency? How does Node.js handle it?
A A circular dependency occurs when two or more modules require each other. Node.js does not get stuck in an infinite loop; instead, it returns the 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.
Q Why must import statements be written at the top level in ESM?
A ESM is statically analyzed, so dependencies are determined during the compilation phase, which facilitates tree shaking and optimization. For dynamic loading scenarios, you can use the import() function.
Q What happens when you use require to load a JSON file?
A Node.js reads the JSON file and automatically parses it using JSON.parse(), returning a parsed JavaScript object. This is commonly used for loading configuration files.

📖 Summary


📝 Exercises

  1. Create the calculator.js module, export the four functions add, subtract, multiply, and divide, then require and use them in main.js
  2. Convert the previous question to an ESM version: Use the export syntax and the .mjs suffix, and load it using import.
  3. Write code to verify the existence of require.cache: After requiring a module, print the information about that module from require.cache.
  4. 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.
  5. Use the path and os modules to print the current operating system platform, the number of CPU cores, and the absolute path of the current directory.
Web-Tutorial.com

Web-Tutorial Tech Team

A team of developers maintaining programming tutorials. Each tutorial is written and reviewed by developers with expertise in that field. We work to keep our content accurate and reliable — if you spot an issue, please let us know.

100%

🙏 帮我们做得更好

我们是刚上线的编程教程站,几个人的小团队,精力有限。页面虽经检查,难免还有疏漏——链接失效、排版错乱、内容有误、语言生硬……

如果您发现了,麻烦告诉我们,我们会在收到反馈后第一时间进行修复,再次感谢您的光临 🙏