NODE: Node.js Modules

Modules are the fundamental building blocks of Node.js applications. This post covers CommonJS modules, ES Modules, built-in core modules, and how the Node.js module system resolves and caches dependencies.

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.

md
+------------------+     +------------------+     +------------------+
|   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

javascript
// 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:

javascript
// greet.js
module.exports = function greet(name) {
    return `Hello, ${name}!`;
};

Importing a Module

javascript
// 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:

javascript
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 .mjs extension, or
  • Set "type": "module" in your package.json

Exporting with ESM

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

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

FeatureCommonJS (CJS)ES Modules (ESM)
Syntaxrequire() / module.exportsimport / export
LoadingSynchronousAsynchronous
Default in Node.jsYes (legacy)Yes (modern)
Tree shakingNoYes
Top-level awaitNoYes
Browser compatibleNoYes

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.

md
+----------------------------------------------+
|           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)

javascript
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

javascript
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

javascript
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

javascript
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.

javascript
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:

md
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:

  1. Exact match: ./math
  2. With extensions: ./math.js, ./math.json, ./math.node
  3. 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.

javascript
// counter.js
let count = 0;

module.exports = {
    increment() { count++; },
    getCount() { return count; }
};

javascript
// 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:

md
utils/
├── index.js      ← Public API
├── date.js
├── string.js
└── number.js

javascript
// utils/index.js
const { formatDate } = require('./date');
const { capitalize } = require('./string');
const { clamp } = require('./number');

module.exports = { formatDate, capitalize, clamp };

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