Tutorial Node.js JavaScript Backend Advanced Streams Concurrency Developer Tools

Free Node.js Advanced Patterns Cheat Sheet Online — Interactive Deep-Dive Reference

· 26 min read

Node.js is often introduced as JavaScript on the server. That description is accurate but insufficient. Under the familiar syntax lies a platform engineered for high-throughput I/O, precise memory control, and sophisticated concurrency. The developers who master Node.js are not those who memorize the most npm packages. They are the ones who understand streams, event emitters, worker threads, binary data, cryptography, and the module system at a deep level. These are the nodejs advanced patterns that separate scripts from systems.

Our free interactive Node.js Advanced Patterns Cheat Sheet maps these patterns into a browsable, searchable reference. Every entry includes a concise explanation, a copyable code example, and cross-references to related concepts. The Terminal Matrix aesthetic — deep terminal-black backgrounds, phosphor-green syntax highlighting, amber status indicators, and monospace typography — turns API exploration into a systems administration experience. Everything runs client-side in your browser with no server interaction, no signup, and no data collection.

Skip ahead: If you want to explore the tool while reading, open the Node.js Advanced Patterns Cheat Sheet in another tab. Use it to test the code examples in this article in real time.

Streams in Node.js — Readable, Writable, Transform, and Backpressure

Streams are the single most important abstraction in Node.js for building scalable data pipelines. They process data incrementally in chunks, keeping memory usage constant regardless of input size. A server handling a 10-gigabyte file upload does not load the entire file into RAM. It reads a chunk, processes it, and discards it before reading the next. This is why streams are fundamental to nodejs backend patterns that handle real-world data volumes.

Creating a Readable Stream

A Readable stream produces data. You can create one from a string, buffer, or by implementing the _read method for custom data sources. The stream module provides both flowing mode (data is pushed automatically) and paused mode (you call read() explicitly).

const { Readable } = require('stream');

// Readable from an array of strings
const source = ['line 1\n', 'line 2\n', 'line 3\n'];
const readable = new Readable({
  read() {
    const chunk = source.shift();
    this.push(chunk || null); // null signals end of stream
  }
});

readable.on('data', chunk => process.stdout.write(chunk));
readable.on('end', () => console.log('Stream ended'));

For file-based readable streams, fs.createReadStream is the standard approach. It automatically handles chunking, encoding, and backpressure when piped to a destination.

Writing to a Writable Stream

A Writable stream consumes data. Implementing a custom writable requires defining the _write method, which receives chunks and a callback to signal completion or error.

const { Writable } = require('stream');

const writable = new Writable({
  write(chunk, encoding, callback) {
    console.log('Received:', chunk.toString().trim());
    callback(); // Signal success
  }
});

writable.write('Hello');
writable.write('World');
writable.end();

The write() method returns a boolean indicating whether the internal buffer has room for more data. When it returns false, the producer should pause until the 'drain' event fires. This is the core of backpressure handling.

Transform Streams and Pipelines

Transform streams are both readable and writable. They read data, transform it, and pass it along. This makes them ideal for compression, encryption, parsing, and format conversion. The nodejs pipeline function connects streams safely, handling errors and cleanup automatically.

const { Transform, pipeline } = require('stream');
const fs = require('fs');
const zlib = require('zlib');

const upperCase = new Transform({
  transform(chunk, encoding, callback) {
    this.push(chunk.toString().toUpperCase());
    callback();
  }
});

pipeline(
  fs.createReadStream('input.txt'),
  upperCase,
  zlib.createGzip(),
  fs.createWriteStream('output.txt.gz'),
  (err) => {
    if (err) console.error('Pipeline failed:', err);
    else console.log('Pipeline succeeded');
  }
);

The promise-based pipeline API (require('stream/promises')) is the modern approach for async/await codebases.

Handling Backpressure

Backpressure is what prevents a fast producer from overwhelming a slow consumer. Without it, memory usage grows until the process crashes. Node.js streams handle backpressure through the return value of write() and the 'drain' event.

const fs = require('fs');
const readable = fs.createReadStream('huge-file.txt');
const writable = fs.createWriteStream('output.txt');

function pump() {
  let chunk;
  while ((chunk = readable.read()) !== null) {
    const canContinue = writable.write(chunk);
    if (!canContinue) {
      writable.once('drain', pump);
      return;
    }
  }
}

readable.on('readable', pump);
readable.on('end', () => writable.end());

In practice, you rarely write this manually. stream.pipeline() and readable.pipe() handle backpressure for you. But understanding the mechanism is essential for debugging memory leaks and writing custom stream implementations.

Concurrency Models — Cluster and Worker Threads

Node.js runs JavaScript on a single thread with an event loop. This is excellent for I/O-bound work but a bottleneck for CPU-bound tasks. Two modules solve this: cluster for scaling I/O across processes, and worker_threads for parallelizing CPU work.

Scaling with the Cluster Module

The nodejs cluster module creates multiple Node.js processes that share the same server port. Each process runs on a separate CPU core, utilizing the full capacity of multi-core machines. The primary process forks worker processes, and the operating system load-balances incoming connections.

const cluster = require('cluster');
const http = require('http');
const os = require('os');

if (cluster.isPrimary) {
  console.log(`Primary ${process.pid} is running`);
  const numCPUs = os.cpus().length;
  for (let i = 0; i < numCPUs; i++) {
    cluster.fork();
  }
  cluster.on('exit', (worker) => {
    console.log(`Worker ${worker.process.pid} died, restarting...`);
    cluster.fork();
  });
} else {
  http.createServer((req, res) => {
    res.writeHead(200);
    res.end(`Handled by worker ${process.pid}\n`);
  }).listen(3000);
  console.log(`Worker ${process.pid} started`);
}

Cluster is the right choice for HTTP servers, WebSocket handlers, and any I/O-bound application that needs to handle more concurrent connections than a single process can manage.

Worker Threads for CPU-Intensive Tasks

The nodejs worker threads module creates threads that run JavaScript in parallel within the same process. Unlike cluster, worker threads share memory and can exchange data efficiently via ArrayBuffer and SharedArrayBuffer. This makes them ideal for CPU-intensive tasks like image resizing, video encoding, complex mathematical computations, and large dataset transformations.

const { Worker, isMainThread, parentPort, workerData } = require('worker_threads');

if (isMainThread) {
  const worker = new Worker(__filename, {
    workerData: [1, 2, 3, 4, 5, 6, 7, 8, 9, 10]
  });
  worker.on('message', result => console.log('Sum:', result));
  worker.on('error', err => console.error(err));
} else {
  const sum = workerData.reduce((a, b) => a + b, 0);
  parentPort.postMessage(sum);
}

Worker threads have their own event loop and V8 isolate, so they do not block the main thread. However, they cannot share JavaScript objects directly — data must be serialized or transferred via ArrayBuffer.

SharedArrayBuffer and Atomics

Nodejs sharedarraybuffer allows multiple threads to access the same memory region. Combined with the Atomics API, it enables lock-free concurrent programming. This is advanced territory, but essential for high-performance parallel algorithms.

const { Worker, isMainThread, workerData } = require('worker_threads');

if (isMainThread) {
  const sharedBuffer = new SharedArrayBuffer(4);
  const counter = new Int32Array(sharedBuffer);
  for (let i = 0; i < 4; i++) {
    new Worker(__filename, { workerData: sharedBuffer });
  }
  setTimeout(() => console.log('Final counter:', counter[0]), 1000);
} else {
  const counter = new Int32Array(workerData);
  for (let i = 0; i < 10000; i++) {
    Atomics.add(counter, 0, 1);
  }
}

Atomics operations are guaranteed to be atomic and ordered, preventing race conditions without explicit locks. This pattern is used in high-performance computing, game engines, and real-time data processing.

Event-Driven Architecture — EventEmitter and Process

Node.js is built on events. The nodejs event emitter class is the backbone of streams, servers, and processes. Understanding EventEmitter is essential for building decoupled, reactive systems.

Building Custom EventEmitters

Custom EventEmitters enable loose coupling between components. A logger module can emit events without knowing which handlers are attached. A job queue can notify listeners of state changes without direct references.

const EventEmitter = require('events');

class JobQueue extends EventEmitter {
  constructor() {
    super();
    this.jobs = [];
  }
  add(job) {
    this.jobs.push(job);
    this.emit('jobAdded', job);
    this.process();
  }
  process() {
    while (this.jobs.length > 0) {
      const job = this.jobs.shift();
      this.emit('jobStarted', job);
      // ... do work
      this.emit('jobCompleted', job);
    }
  }
}

const queue = new JobQueue();
queue.on('jobAdded', job => console.log('Added:', job.id));
queue.on('jobCompleted', job => console.log('Done:', job.id));
queue.add({ id: 1, data: 'process-me' });

The once() method registers a one-time listener that automatically removes itself after firing. prependListener() adds a handler to the front of the queue. removeListener() and off() detach handlers. These methods give fine-grained control over event propagation.

Process Signals and Lifecycle

The process object is a global EventEmitter that exposes the lifecycle of the Node.js process. Signals like SIGINT (Ctrl+C), SIGTERM (graceful shutdown), and uncaughtException require careful handling in production applications.

process.on('SIGTERM', () => {
  console.log('SIGTERM received, shutting down gracefully...');
  server.close(() => {
    db.disconnect(() => {
      process.exit(0);
    });
  });
});

process.on('uncaughtException', (err) => {
  console.error('Uncaught exception:', err);
  process.exit(1); // Restart via process manager
});

process.on('unhandledRejection', (reason, promise) => {
  console.error('Unhandled rejection at:', promise, 'reason:', reason);
});

Graceful shutdown is critical for zero-downtime deployments. The SIGTERM handler should close servers, flush logs, disconnect from databases, and then exit cleanly.

Error Handling in EventEmitters

EventEmitters treat 'error' events specially. If an 'error' event is emitted and no listener is attached, Node.js throws the error and crashes the process. Always attach an error handler.

const emitter = new EventEmitter();

emitter.on('error', (err) => {
  console.error('Emitter error:', err.message);
});

emitter.emit('error', new Error('Something went wrong'));
// Process continues because error handler is attached

For streams, the 'error' event fires on read/write failures. For servers, it fires on listen failures. For child processes, it fires on spawn errors. The pattern is consistent across the entire Node.js ecosystem.

Working with Binary Data — Buffer and TypedArrays

JavaScript has no native byte type. The nodejs buffer class fills this gap, providing a fixed-length sequence of bytes for binary data manipulation. Buffers are essential for file I/O, network protocols, cryptography, and image processing.

Creating and Manipulating Buffers

Buffers can be created from strings, arrays, other Buffers, or allocated directly. They support random access, slicing, and encoding conversion.

const buf1 = Buffer.from('Hello', 'utf8');
const buf2 = Buffer.alloc(10); // Zero-filled
const buf3 = Buffer.allocUnsafe(10); // Uninitialized, faster

buf1.write('Hi', 0, 'utf8');
console.log(buf1.toString('hex')); // '4869206c6c6f'
console.log(buf1.length); // 5

const slice = buf1.slice(0, 2);
console.log(slice.toString()); // 'Hi'

Buffer.allocUnsafe is faster but may contain old memory contents. Use it only when you will immediately overwrite the entire buffer. For security-sensitive code, always use Buffer.alloc.

Buffer vs String Performance

Strings in JavaScript are immutable UTF-16 sequences. Every string operation creates a new string. Buffers are mutable byte arrays. For binary data, buffers are both faster and more memory-efficient. For text processing, strings are more convenient but incur conversion overhead.

const fs = require('fs');

// Fast: read as Buffer, process bytes directly
const buffer = fs.readFileSync('data.bin');
for (let i = 0; i < buffer.length; i++) {
  buffer[i] = buffer[i] ^ 0xFF; // XOR each byte
}

// Slower: read as string, convert back and forth
const text = fs.readFileSync('data.bin', 'utf8');
const modified = Buffer.from(text, 'utf8').map(b => b ^ 0xFF);

For network protocols and file formats, work in Buffers as long as possible. Convert to strings only at presentation boundaries.

File System Patterns — Path and fs.promises

Modern Node.js file system code uses the nodejs fs promises API and the path module for cross-platform compatibility. These patterns replace callback-based code and manual path concatenation.

Path Manipulation Best Practices

The nodejs path module handles platform differences automatically. Windows uses backslashes. POSIX uses forward slashes. path.join concatenates segments. path.resolve returns absolute paths.

const path = require('path');

const configPath = path.join(__dirname, 'config', 'app.json');
const absolute = path.resolve('src', 'utils', 'helper.js');

path.extname('image.png'); // '.png'
path.basename('/docs/readme.md', '.md'); // 'readme'
path.dirname('/docs/readme.md'); // '/docs'
path.parse('/home/user/file.txt');
// { root: '/', dir: '/home/user', base: 'file.txt', ext: '.txt', name: 'file' }

path.normalize('foo//bar/../baz'); // 'foo/baz'
path.relative('/data/orandea/test/aaa', '/data/orandea/impl/bbb');
// '../../impl/bbb'

Always use path.join or path.resolve instead of string concatenation. This ensures your code works on Windows, macOS, and Linux without modification.

Modern Async File Operations

The fs.promises API provides promise-based versions of all file system operations. Combined with async/await, it produces clean, sequential-looking code that is fully non-blocking.

const fsp = require('fs').promises;

async function processDirectory(dirPath) {
  const entries = await fsp.readdir(dirPath, { withFileTypes: true });
  for (const entry of entries) {
    const fullPath = path.join(dirPath, entry.name);
    if (entry.isDirectory()) {
      console.log('Directory:', entry.name);
      await processDirectory(fullPath);
    } else {
      const content = await fsp.readFile(fullPath, 'utf8');
      console.log('File:', entry.name, 'Size:', content.length);
    }
  }
}

await processDirectory('./src');

The withFileTypes option returns fs.Dirent objects instead of strings, allowing you to check isDirectory() and isFile() without additional stat calls. This is a significant performance optimization for directory traversal.

Watching Files for Changes

fs.watch and fs.watchFile monitor files and directories for changes. fs.watch uses platform-specific mechanisms (inotify on Linux, FSEvents on macOS, ReadDirectoryChangesW on Windows) and is more efficient. fs.watchFile polls the file and is more portable but slower.

const fs = require('fs');

const watcher = fs.watch('./config.json', (eventType, filename) => {
  console.log(`Event: ${eventType}, File: ${filename}`);
  if (eventType === 'change') {
    reloadConfig();
  }
});

// Clean up on shutdown
process.on('SIGINT', () => {
  watcher.close();
  process.exit(0);
});

For production file watching, consider the chokidar npm package, which normalizes behavior across platforms and handles edge cases like rapid successive changes.

Cryptography in Node.js

The nodejs crypto module provides cryptographic primitives: hashing, HMAC, symmetric encryption, asymmetric encryption, key derivation, and secure randomness. For security-sensitive applications, understanding these APIs is non-negotiable.

Hashing and HMAC

Hashing converts data into a fixed-length digest. It is used for file integrity checks, cache keys, and non-password verification. HMAC (Hash-based Message Authentication Code) combines a hash function with a secret key, providing both integrity and authenticity.

const crypto = require('crypto');

// SHA-256 hash
const hash = crypto.createHash('sha256')
  .update('important-data')
  .digest('hex');

// HMAC with secret key
const hmac = crypto.createHmac('sha256', 'secret-key')
  .update('important-data')
  .digest('hex');

console.log('Hash:', hash);
console.log('HMAC:', hmac);

Never use MD5 or SHA-1 for security purposes. They are cryptographically broken. Use SHA-256 or SHA-3 for hashing, and SHA-256 for HMAC.

Encryption and Decryption

The crypto module supports symmetric encryption (AES) and asymmetric encryption (RSA). Symmetric encryption uses the same key for encryption and decryption. Asymmetric encryption uses a public key to encrypt and a private key to decrypt.

const crypto = require('crypto');

const algorithm = 'aes-256-gcm';
const key = crypto.randomBytes(32);
const iv = crypto.randomBytes(16);

function encrypt(text) {
  const cipher = crypto.createCipheriv(algorithm, key, iv);
  let encrypted = cipher.update(text, 'utf8', 'hex');
  encrypted += cipher.final('hex');
  const authTag = cipher.getAuthTag();
  return { encrypted, iv: iv.toString('hex'), authTag: authTag.toString('hex') };
}

function decrypt(encrypted, ivHex, authTagHex) {
  const decipher = crypto.createDecipheriv(algorithm, key, Buffer.from(ivHex, 'hex'));
  decipher.setAuthTag(Buffer.from(authTagHex, 'hex'));
  let decrypted = decipher.update(encrypted, 'hex', 'utf8');
  decrypted += decipher.final('utf8');
  return decrypted;
}

const result = encrypt('sensitive data');
console.log(decrypt(result.encrypted, result.iv, result.authTag));

AES-256-GCM is the recommended symmetric cipher. It provides both confidentiality and authentication, preventing tampering even if the ciphertext is intercepted.

Secure Randomness and Key Derivation

Never use Math.random for cryptographic purposes. The nodejs crypto module provides cryptographically secure random bytes. For password storage, use scrypt or pbkdf2 — key derivation functions designed to be computationally expensive.

const crypto = require('crypto');

// Secure random values
const randomBytes = crypto.randomBytes(32);
const uuid = crypto.randomUUID();

// Password hashing with scrypt
const password = 'user-password';
const salt = crypto.randomBytes(16);
const keyLength = 64;

crypto.scrypt(password, salt, keyLength, (err, derivedKey) => {
  if (err) throw err;
  // Store salt and derivedKey in database
  console.log('Derived key:', derivedKey.toString('hex'));
});

// Key derivation with pbkdf2
const iterations = 100000;
crypto.pbkdf2(password, salt, iterations, keyLength, 'sha512', (err, derivedKey) => {
  if (err) throw err;
  console.log('PBKDF2 key:', derivedKey.toString('hex'));
});

scrypt is memory-hard, making it resistant to GPU and ASIC attacks. pbkdf2 is CPU-hard and widely supported. Both are suitable for password hashing. Argon2 is the modern recommendation but requires external packages.

Network Protocols — HTTP/2 and Net

Node.js supports modern network protocols including nodejs http2 and raw TCP sockets via the net module. These APIs enable high-performance servers and custom protocol implementations.

HTTP/2 Server Push

HTTP/2 multiplexes multiple streams over a single connection, reducing latency. Server push allows the server to send resources proactively before the client requests them.

const http2 = require('http2');
const fs = require('fs');

const server = http2.createSecureServer({
  key: fs.readFileSync('server.key'),
  cert: fs.readFileSync('server.cert')
});

server.on('stream', (stream, headers) => {
  const path = headers[':path'];
  if (path === '/') {
    stream.respond({ 'content-type': 'text/html', ':status': 200 });
    stream.end('');

    // Push app.js proactively
    stream.pushStream({ ':path': '/app.js' }, (err, pushStream) => {
      if (err) throw err;
      pushStream.respond({ 'content-type': 'application/javascript', ':status': 200 });
      pushStream.end(fs.readFileSync('app.js'));
    });
  }
});

server.listen(8443);

HTTP/2 requires TLS in most browsers. The http2 module supports both secure and insecure servers, but production deployments should always use TLS.

Raw TCP Sockets

The net module provides raw TCP socket access. This is useful for protocols other than HTTP, such as databases, message queues, and custom binary protocols.

const net = require('net');

const server = net.createServer((socket) => {
  console.log('Client connected:', socket.remoteAddress);

  socket.on('data', (data) => {
    console.log('Received:', data.toString());
    socket.write('Echo: ' + data);
  });

  socket.on('end', () => {
    console.log('Client disconnected');
  });
});

server.listen(9000, () => {
  console.log('TCP server listening on port 9000');
});

// Client
const client = net.createConnection({ port: 9000 }, () => {
  client.write('Hello from client');
});
client.on('data', data => console.log(data.toString()));

TCP sockets provide full control over the connection lifecycle. You can implement custom framing, heartbeat mechanisms, and backpressure handling that would be impossible with higher-level protocols.

Module System Deep-Dive

The nodejs module system has evolved from CommonJS to ES Modules. Understanding both systems, their interoperability, and advanced features like conditional exports is essential for modern Node.js development.

CommonJS vs ES Modules

CommonJS uses require() and module.exports. It is synchronous, dynamically evaluated, and copies exports at require time. ES Modules use import and export. They are statically analyzed, support live bindings, and enable tree shaking.

// CommonJS
const fs = require('fs');
module.exports = { readConfig };

// ES Module
import fs from 'fs';
export function readConfig() { /* ... */ }
export default readConfig;

Node.js determines module type by file extension (.mjs for ESM, .cjs for CJS) or by the "type" field in package.json. Mixed projects require careful attention to interoperability rules.

createRequire and import.meta

ES Modules do not have require(), __dirname, or __filename. The createRequire function creates a require function inside an ES module. import.meta.url provides the module's URL, from which you can derive __dirname.

import { createRequire } from 'module';
import { fileURLToPath } from 'url';
import { dirname } from 'path';

const require = createRequire(import.meta.url);
const __filename = fileURLToPath(import.meta.url);
const __dirname = dirname(__filename);

// Now you can use require in ESM
const config = require('./config.json');
console.log('Dirname:', __dirname);

This pattern is essential when migrating CommonJS code to ES Modules or when an ES module needs to load JSON files or CJS packages.

Conditional Exports and Subpath Imports

package.json conditional exports allow a package to expose different entry points for different environments — require vs import, development vs production, Node.js vs browser.

// package.json
{
  "name": "my-package",
  "exports": {
    ".": {
      "import": "./index.mjs",
      "require": "./index.cjs"
    },
    "./utils": {
      "import": "./utils.mjs",
      "require": "./utils.cjs"
    }
  }
}

// Consumer (ESM)
import { helper } from 'my-package/utils';

Subpath imports ("#name" mappings in package.json) let you define private aliases for internal modules, avoiding deep relative paths like ../../../utils/helper.

Performance and Diagnostics

Node.js provides powerful APIs for measuring performance, propagating async context, and publishing diagnostic events. These are the tools you need to optimize and debug production applications.

PerformanceObserver and Mark/Measure

The nodejs performance hooks module provides high-resolution timing and performance observation. Use it to measure function execution, database query latency, and HTTP response times.

const { performance, PerformanceObserver } = require('perf_hooks');

const obs = new PerformanceObserver((items) => {
  for (const entry of items.getEntries()) {
    console.log(`${entry.name}: ${entry.duration.toFixed(2)}ms`);
  }
});
obs.observe({ entryTypes: ['measure'] });

function heavyOperation() {
  performance.mark('start');
  for (let i = 0; i < 1e7; i++) {} // Simulate work
  performance.mark('end');
  performance.measure('heavyOperation', 'start', 'end');
}

heavyOperation();

PerformanceObserver can also observe 'function' entries (when using timerify), 'gc' entries for garbage collection events, and 'http' entries for request timing.

AsyncLocalStorage for Context Propagation

AsyncLocalStorage enables you to store and access data throughout the lifetime of an async call chain. This is invaluable for request tracing, user context, and transaction IDs that need to propagate through callbacks and promises without explicit passing.

const { AsyncLocalStorage } = require('async_hooks');

const asyncLocalStorage = new AsyncLocalStorage();

function logWithContext(message) {
  const context = asyncLocalStorage.getStore();
  console.log(`[Request ${context?.requestId}] ${message}`);
}

function handleRequest(requestId) {
  asyncLocalStorage.run({ requestId }, () => {
    logWithContext('Starting request');
    setTimeout(() => {
      logWithContext('After timeout');
      Promise.resolve().then(() => {
        logWithContext('In promise');
      });
    }, 100);
  });
}

handleRequest('abc-123');
// [Request abc-123] Starting request
// [Request abc-123] After timeout
// [Request abc-123] In promise

AsyncLocalStorage replaces the older async_hooks-based approaches with a simpler, safer API. It is the standard for request context in modern Node.js frameworks.

Diagnostics Channel

The nodejs diagnostics channel module provides a publish-subscribe mechanism for diagnostic events. Libraries can publish events, and monitoring tools can subscribe without modifying the library code.

const { channel } = require('diagnostics_channel');

const requestChannel = channel('my-app.request');

// Subscriber (monitoring tool)
requestChannel.subscribe((message) => {
  console.log('Request event:', message.url, 'Duration:', message.duration);
});

// Publisher (application code)
function processRequest(url) {
  const start = Date.now();
  // ... process request
  const duration = Date.now() - start;
  if (requestChannel.hasSubscribers) {
    requestChannel.publish({ url, duration });
  }
}

processRequest('/api/users');

Diagnostics Channel is used internally by Node.js for HTTP, DNS, and Net events. It enables zero-overhead observability when no subscribers are attached.

Advanced APIs and Utilities

Node.js includes several specialized modules for metaprogramming, debugging, and utility functions. These APIs are less commonly used but powerful in the right context.

REPL and VM Module

The nodejs repl module lets you embed an interactive JavaScript shell in your application. The nodejs vm module provides a sandboxed context for executing JavaScript code with controlled globals.

const repl = require('repl');
const vm = require('vm');

// Embedded REPL
repl.start({
  prompt: 'my-app> ',
  eval: (cmd, context, filename, callback) => {
    try {
      const result = vm.runInNewContext(cmd, { console, Math });
      callback(null, result);
    } catch (err) {
      callback(err);
    }
  }
});

// VM Context for sandboxed execution
const context = vm.createContext({ console, x: 2 });
vm.runInContext('console.log(x + 3); x = 10;', context);
console.log(context.x); // 10

The VM module is useful for plugin systems, code evaluation, and testing. Note that VM contexts are not fully secure sandboxes — they can still consume CPU and memory. For untrusted code, use separate processes or WebAssembly.

util.promisify and util.inspect

Nodejs util promisify converts callback-based functions into promise-based ones. util.inspect provides deep, customizable object inspection for logging and debugging.

const util = require('util');
const fs = require('fs');

// Convert callback API to promise
const readFile = util.promisify(fs.readFile);
const data = await readFile('config.json', 'utf8');

// Deep object inspection
const obj = { nested: { array: [1, 2, 3], date: new Date() } };
console.log(util.inspect(obj, { depth: null, colors: true }));

// Custom promisify (for APIs with multiple success arguments)
function customFn(arg, callback) {
  callback(null, 'result1', 'result2');
}
customFn[util.promisify.custom] = (arg) => {
  return new Promise((resolve) => {
    customFn(arg, (err, ...results) => resolve(results));
  });
};
const results = await util.promisify(customFn)('test');
console.log(results); // ['result1', 'result2']

Since Node.js 8, many core modules provide native promise APIs (fs.promises, stream/promises, timers/promises). Use these when available. Use util.promisify for legacy callback APIs and third-party modules.

AbortController and Web Streams

The nodejs abortcontroller API provides a standard way to cancel asynchronous operations. It is supported by fetch, streams, timers, and many third-party libraries.

const controller = new AbortController();
const { signal } = controller;

// Cancel a fetch request after 5 seconds
setTimeout(() => controller.abort(), 5000);

try {
  const response = await fetch('https://api.example.com/data', { signal });
  const data = await response.json();
} catch (err) {
  if (err.name === 'AbortError') {
    console.log('Request was cancelled');
  }
}

// AbortController with streams
const readStream = fs.createReadStream('large-file.txt');
readStream.on('close', () => console.log('Stream closed'));

signal.addEventListener('abort', () => {
  readStream.destroy();
});

AbortController is the modern replacement for ad-hoc cancellation mechanisms. It follows the DOM standard, making code portable between Node.js and browsers.

Summary / TL;DR

PatternModuleUse CaseKey API
StreamsstreamMemory-efficient data processingpipeline(), Transform, createReadStream
ClusterclusterScale I/O across CPU corescluster.fork(), isPrimary
Worker Threadsworker_threadsCPU-intensive parallel tasksnew Worker(), SharedArrayBuffer
EventEmittereventsDecoupled event-driven architectureon(), emit(), once()
BufferbufferBinary data manipulationBuffer.from(), alloc(), toString()
CryptocryptoHashing, encryption, randomnesscreateHash, scrypt, randomBytes
HTTP/2http2Multiplexed, low-latency serverscreateSecureServer(), pushStream
ES ModulesmoduleModern module systemimport/export, createRequire
Performanceperf_hooksTiming and performance observationPerformanceObserver, mark/measure
Async Contextasync_hooksRequest context propagationAsyncLocalStorage
Diagnosticsdiagnostics_channelObservability and monitoringchannel.subscribe/publish
VM/REPLvm, replSandboxed execution, interactive shellsrunInContext(), repl.start()
UtilitiesutilPromisify, inspect, formattingpromisify(), inspect()
CancellationglobalsCancel async operationsAbortController, AbortSignal

Try the Free Interactive Cheat Sheet

This article covers the concepts, but the Node.js Advanced Patterns Cheat Sheet puts every pattern at your fingertips. Open it in your browser, search for any API, copy code examples with one click, and browse by category. It is free, requires no registration, and runs entirely client-side.

Node.js advanced patterns are not trivia. They are the tools that turn a script into a system, a prototype into a product, and a developer into an engineer. Master streams and you can process infinite data. Master concurrency and you can saturate every CPU core. Master cryptography and you can protect user data. Master the module system and you can ship libraries the ecosystem depends on. These patterns are worth knowing deeply.

Related Resources

Every tool in the DevToolkit collection is free, requires no signup, and runs entirely in your browser. Build faster by keeping the right reference at your fingertips.

Found this useful? Check out our free developer tools or browse more articles.