Skip to main content

Documentation Index

Fetch the complete documentation index at: https://resources.devweekends.com/llms.txt

Use this file to discover all available pages before exploring further.

Node.js Module System

Modules in Node.js

Modules are the building blocks of any Node.js application. Think of them like LEGO bricks: each module is a self-contained piece with a specific shape and purpose. You snap them together to build something larger, and you can swap one brick for another without rebuilding the whole structure. Without modules, you would end up with one massive file where every function can see and accidentally break every other function—the programming equivalent of storing your entire house in a single room. Node.js uses the CommonJS module system by default, though it also supports ES Modules (ESM) in newer versions.

The require Function

To include a module, use the require() function with the name of the module.
const fs = require('fs'); // Built-in module

Creating Custom Modules

Let’s create a simple module that exports some math functions. math.js
const add = (a, b) => a + b;
const subtract = (a, b) => a - b;

// Exporting functions
module.exports = {
  add,
  subtract
};
app.js
const math = require('./math');

console.log(math.add(5, 3));      // Output: 8
console.log(math.subtract(5, 3)); // Output: 2

Alternative Export Syntax

You can also attach properties directly to exports (which is a shorthand for module.exports).
exports.multiply = (a, b) => a * b;
exports.divide = (a, b) => a / b;
Pitfall: Never reassign exports directly (e.g., exports = { multiply, divide }). The exports variable is just a reference to module.exports. Reassigning it breaks the reference, and your exports silently disappear. Always use module.exports = ... when exporting a single object or class, and exports.name = ... only when attaching individual properties.

Module Wrapper Function

Under the hood, Node.js doesn’t execute your code directly. It wraps it inside a function wrapper:
(function(exports, require, module, __filename, __dirname) {
  // Your module code actually lives in here
});
This is why variables defined in a module are scoped to that module (private) rather than being global. This wrapper function is also why you have access to __filename and __dirname without declaring them—they are parameters injected by Node.js, not magic globals. Understanding this wrapper is key to understanding why var x = 5 in a module does not pollute the global scope the way it would in a browser script tag.

Built-in Modules

Node.js comes with many useful built-in modules. We will explore these in depth in later chapters, but here are a few common ones:
  • fs: File system operations.
  • http: Create HTTP servers.
  • path: Utilities for working with file and directory paths.
  • os: Operating system information.
  • events: Event emitter.

Example: The os Module

const os = require('os');

console.log('Platform:', os.platform());
console.log('Architecture:', os.arch());
console.log('Free Memory:', os.freemem());
console.log('Total Memory:', os.totalmem());
console.log('Uptime:', os.uptime());

ES Modules (import/export)

Node.js has added support for ES Modules, which is the standard in modern JavaScript (and browsers). To use ES Modules, you can either:
  1. Use the .mjs extension for your files.
  2. Add "type": "module" to your package.json.
math.mjs
export const add = (a, b) => a + b;
export default function log(msg) {
  console.log(msg);
}
app.mjs
import log, { add } from './math.mjs';

log('Result: ' + add(2, 3));

CommonJS vs ES Modules Comparison

FeatureCommonJSES Modules
Syntaxrequire() / module.exportsimport / export
LoadingSynchronousAsynchronous
File Extension.js or .cjs.mjs or .js with “type”: “module”
Top-level awaitNot supportedSupported
Dynamic importsrequire(variable)import(variable)
Tree shakingLimitedSupported
Browser supportNoYes
__dirname / __filenameAvailable directlyMust use import.meta.url + fileURLToPath
Conditional exportsif (x) require('a') works anywhereStatic imports must be at top level; use dynamic import() for conditional loading
Circular dependency handlingReturns partially-loaded moduleThrows ReferenceError if binding not yet initialized
Default exportmodule.exports = valueexport default value

Module System Decision Framework

Start a new project today? Use ES Modules ("type": "module" in package.json). The ecosystem is moving toward ESM, bundlers work better with it, and it aligns with browser JavaScript. Maintaining an existing CommonJS project? Stay with CommonJS unless you have a compelling reason to migrate. Converting a large codebase is non-trivial: you need to update every require to import, handle __dirname replacements, deal with libraries that only ship CommonJS, and fix circular dependencies that worked under CJS but break under ESM. Publishing an NPM package? Ship both formats using the exports field in package.json (conditional exports). This lets CJS and ESM consumers both use your package without issues. Edge case — CJS/ESM interop: You can import a CommonJS module from ESM (Node.js wraps it as a default export). But you cannot require() an ES module from CommonJS — you must use await import() inside an async function instead. This asymmetry trips up many teams during migration.

Module Resolution: How Node.js Finds Your Modules

When you call require('something'), Node.js follows a specific resolution algorithm. Understanding it prevents mysterious “module not found” errors:
What you writeWhat Node.js doesExample
require('./foo')Looks for ./foo.js, then ./foo/index.js in the same directoryLocal module
require('fs')Checks built-in modules first (always wins over NPM packages with same name)Built-in module
require('lodash')Walks up the directory tree checking node_modules/ at each levelNPM package
require('/abs/path')Uses the absolute path directlyAbsolute path
Edge case — the node_modules crawl: If your project is at /app/src/utils/helper.js and you require('lodash'), Node.js checks: /app/src/utils/node_modules/lodash, then /app/src/node_modules/lodash, then /app/node_modules/lodash, then /node_modules/lodash. This crawl is why deeply nested node_modules caused performance problems in early Node.js and why tools like pnpm use symlinks instead.

Module Caching

Node.js caches modules after the first require(). Subsequent calls return the cached version.
// counter.js
let count = 0;
module.exports = {
  increment: () => ++count,
  getCount: () => count
};

// app.js
const counter1 = require('./counter');
const counter2 = require('./counter');

counter1.increment();
counter1.increment();
console.log(counter2.getCount()); // Output: 2 (same instance!)
Module caching can lead to unexpected behavior if you expect fresh instances. This is the “accidental singleton” pitfall—because require() returns the same cached object every time, any module that holds state (counters, connections, configuration) becomes a shared singleton across your entire app. If you need independent instances, export a factory function or a class instead:
// Instead of exporting an object with state directly:
module.exports = () => {
  let count = 0;
  return { increment: () => ++count, getCount: () => count };
};

Circular Dependencies

Node.js handles circular dependencies, but they can cause issues.
// a.js
console.log('a starting');
exports.done = false;
const b = require('./b.js');
console.log('in a, b.done =', b.done);
exports.done = true;
console.log('a done');

// b.js
console.log('b starting');
exports.done = false;
const a = require('./a.js');
console.log('in b, a.done =', a.done);
exports.done = true;
console.log('b done');

// main.js
const a = require('./a.js');
const b = require('./b.js');

// Output:
// a starting
// b starting
// in b, a.done = false  <-- Incomplete!
// b done
// in a, b.done = true
// a done
Best Practice: Avoid circular dependencies by restructuring your code. Extract shared code into a separate module.

The path Module

The path module provides utilities for working with file and directory paths.
const path = require('path');

// Join paths (handles OS-specific separators)
const filePath = path.join(__dirname, 'data', 'users.json');
console.log(filePath); // /app/data/users.json (Unix) or \app\data\users.json (Windows)

// Resolve to absolute path
const absolute = path.resolve('data', 'users.json');
console.log(absolute); // Full absolute path

// Get file extension
console.log(path.extname('file.txt')); // .txt

// Get filename without extension
console.log(path.basename('file.txt', '.txt')); // file

// Get directory name
console.log(path.dirname('/users/john/file.txt')); // /users/john

// Parse path into components
const parsed = path.parse('/home/user/file.txt');
console.log(parsed);
// {
//   root: '/',
//   dir: '/home/user',
//   base: 'file.txt',
//   ext: '.txt',
//   name: 'file'
// }

The url Module

const { URL } = require('url');

const myUrl = new URL('https://example.com:8080/path?name=John&age=30#section');

console.log(myUrl.href);       // Full URL
console.log(myUrl.protocol);   // https:
console.log(myUrl.host);       // example.com:8080
console.log(myUrl.hostname);   // example.com
console.log(myUrl.port);       // 8080
console.log(myUrl.pathname);   // /path
console.log(myUrl.search);     // ?name=John&age=30
console.log(myUrl.hash);       // #section

// URLSearchParams
console.log(myUrl.searchParams.get('name'));  // John
myUrl.searchParams.append('country', 'USA');
console.log(myUrl.href);

Creating a Reusable Module

Let’s create a practical utility module:
// utils/logger.js
const colors = {
  reset: '\x1b[0m',
  red: '\x1b[31m',
  green: '\x1b[32m',
  yellow: '\x1b[33m',
  blue: '\x1b[34m'
};

const formatDate = () => new Date().toISOString();

const logger = {
  info: (msg) => console.log(`${colors.blue}[INFO]${colors.reset} ${formatDate()} - ${msg}`),
  success: (msg) => console.log(`${colors.green}[SUCCESS]${colors.reset} ${formatDate()} - ${msg}`),
  warn: (msg) => console.log(`${colors.yellow}[WARN]${colors.reset} ${formatDate()} - ${msg}`),
  error: (msg) => console.log(`${colors.red}[ERROR]${colors.reset} ${formatDate()} - ${msg}`)
};

module.exports = logger;

// Usage:
// const logger = require('./utils/logger');
// logger.info('Server started');
// logger.error('Database connection failed');

Summary

  • Node.js uses CommonJS (require/module.exports) by default
  • ES Modules (import/export) are also supported and are becoming the standard for new projects
  • Modules are cached after first load—making every stateful module an accidental singleton
  • Avoid circular dependencies by extracting shared logic into a separate module
  • Use the path module for cross-platform file paths (never concatenate paths with string +)
  • Use the url module for URL parsing and manipulation
  • Node.js wraps every module in a wrapper function, providing local scope and variables like __dirname
  • When starting a new project, prefer ES Modules ("type": "module" in package.json) for tree-shaking support and alignment with the broader JavaScript ecosystem