Every non-trivial Node.js application is built from modules. Modules let you split code into reusable, maintainable pieces, import third-party libraries, and use the rich set of built-in capabilities that Node.js provides out of the box.
What is a Module?
A module is a self-contained unit of code that encapsulates related functionality. In Node.js, every file is treated as its own module by default. Each module has its own scope — variables declared in one module do not leak into another.
+------------------+ +------------------+ +------------------+
| math.js | | utils.js | | app.js |
| | | | | |
| add() | --> | formatDate() | --> | main logic |
| subtract() | | capitalize() | | ties it all |
| | | | | together |
+------------------+ +------------------+ +------------------+
Modules solve the problem of organizing large codebases by separating concerns and enabling reuse.
CommonJS Modules (CJS)
The original and still widely used module system in Node.js is CommonJS (CJS). It uses require() to import and module.exports to export.
Exporting from a Module
// math.js
function add(a, b) {
return a + b;
}
function subtract(a, b) {
return a - b;
}
module.exports = { add, subtract };
You can also export a single value:
// greet.js
module.exports = function greet(name) {
return `Hello, ${name}!`;
};
Importing a Module
// app.js
const math = require('./math');
console.log(math.add(5, 3)); // 8
console.log(math.subtract(10, 4)); // 6
You can also destructure the import:
const { add, subtract } = require('./math');
console.log(add(2, 3)); // 5
console.log(subtract(8, 3)); // 5
ES Modules (ESM)
Node.js also supports ES Modules (ESM), the modern JavaScript module standard introduced in ES6. ESM uses import and export syntax.
To use ESM in Node.js, either:
- Name your file with
.mjsextension, or - Set
"type": "module"in yourpackage.json
Exporting with ESM
// math.mjs
export function add(a, b) {
return a + b;
}
export function subtract(a, b) {
return a - b;
}
export default function multiply(a, b) {
return a * b;
}
Importing with ESM
// app.mjs
import multiply, { add, subtract } from './math.mjs';
console.log(add(2, 3)); // 5
console.log(subtract(10, 4)); // 6
console.log(multiply(3, 4)); // 12
CJS vs ESM Comparison
| Feature | CommonJS (CJS) | ES Modules (ESM) |
|---|---|---|
| Syntax | require() / module.exports | import / export |
| Loading | Synchronous | Asynchronous |
| Default in Node.js | Yes (legacy) | Yes (modern) |
| Tree shaking | No | Yes |
Top-level await | No | Yes |
| Browser compatible | No | Yes |
Both systems are valid. CommonJS remains common in older codebases and npm packages. ESM is the future direction of JavaScript modules.
Built-in Core Modules
Node.js ships with a large set of built-in (core) modules that provide essential functionality without requiring any installation.
+----------------------------------------------+
| Node.js Core Modules |
| |
| fs - File system operations |
| path - File path utilities |
| http - HTTP server and client |
| https - HTTPS server and client |
| os - Operating system information |
| events - EventEmitter pattern |
| stream - Streaming data |
| crypto - Cryptographic utilities |
| url - URL parsing and formatting |
| util - Utility functions |
| child_process - Spawn subprocesses |
+----------------------------------------------+
The `fs` Module (File System)
const fs = require('fs');
// Read a file asynchronously
fs.readFile('./data.txt', 'utf8', (err, data) => {
if (err) throw err;
console.log(data);
});
// Write a file
fs.writeFile('./output.txt', 'Hello, Node!', (err) => {
if (err) throw err;
console.log('File written successfully');
});
The `path` Module
const path = require('path');
const filePath = path.join('/users', 'amr', 'project', 'index.js');
console.log(filePath); // /users/amr/project/index.js
console.log(path.extname('index.js')); // .js
console.log(path.basename('index.js')); // index.js
console.log(path.dirname('/users/amr/index.js')); // /users/amr
The `http` Module
const http = require('http');
const server = http.createServer((req, res) => {
res.writeHead(200, { 'Content-Type': 'text/plain' });
res.end('Hello from Node.js HTTP server!');
});
server.listen(3000, () => {
console.log('Server listening on port 3000');
});
The `os` Module
const os = require('os');
console.log(os.platform()); // linux, darwin, win32
console.log(os.cpus().length); // number of CPU cores
console.log(os.totalmem()); // total memory in bytes
console.log(os.homedir()); // /Users/amr
The `events` Module
Node.js is built on an event-driven architecture. The events module provides the EventEmitter class, which is the foundation of this pattern.
const EventEmitter = require('events');
class MyEmitter extends EventEmitter {}
const emitter = new MyEmitter();
// Register a listener
emitter.on('data', (message) => {
console.log(`Received: ${message}`);
});
// Emit an event
emitter.emit('data', 'Hello from EventEmitter!');
// Output: Received: Hello from EventEmitter!
Many Node.js built-in modules (HTTP, streams, file system) are built on top of EventEmitter.
How Module Resolution Works
When you call require('something'), Node.js follows a specific lookup order:
require('express')
|
v
1. Is it a core module? (http, fs, path...)
|
v (no)
2. Does the path start with ./ or ../ ?
--> Load relative file
|
v (no)
3. Look in node_modules/
./node_modules/express
../node_modules/express
... (up to root)
|
v
4. Throw MODULE_NOT_FOUND error
For relative paths, Node.js tries:
- Exact match:
./math - With extensions:
./math.js,./math.json,./math.node - As a directory:
./math/index.js
Module Caching
Node.js caches modules after the first require() call. Every subsequent call to require() with the same path returns the cached version rather than re-executing the file.
// counter.js
let count = 0;
module.exports = {
increment() { count++; },
getCount() { return count; }
};
// app.js
const counter = require('./counter');
const counterAgain = require('./counter'); // Returns cached instance
counter.increment();
counter.increment();
console.log(counterAgain.getCount()); // 2 — same instance!
This behavior means modules act as singletons by default.
Creating a Module Index
For large projects, it is common to create an index.js in a folder to expose a clean public API:
utils/
├── index.js ← Public API
├── date.js
├── string.js
└── number.js
// utils/index.js
const { formatDate } = require('./date');
const { capitalize } = require('./string');
const { clamp } = require('./number');
module.exports = { formatDate, capitalize, clamp };
// app.js
const { formatDate, capitalize } = require('./utils');
This pattern keeps imports clean and hides internal implementation details.
Final Thoughts
The Node.js module system is the backbone of every application. Whether you use CommonJS for compatibility or ES Modules for modern syntax, understanding how modules work — how they export, how they resolve, and how they cache — is essential to writing well-structured Node.js applications.
Master modules, and you master how Node.js applications are organized at their foundation.