Mastering Node.js: Architecture, APIs, and the Event Loop Explained

This comprehensive guide explores Node.js’s architecture, core APIs, global variables, built‑in modules, function styles, the event‑loop mechanics, task scheduling, next‑tick and microtasks, libuv’s role, and strategies for off‑loading work, providing developers with a deep understanding of how Node.js runs JavaScript efficiently.

Node Underground
Node Underground
Node Underground
Mastering Node.js: Architecture, APIs, and the Event Loop Explained

Node.js Overview

This article outlines how Node.js works, covering its architecture, API design, global variables, built‑in modules, and the event‑loop.

1.1 Global Node.js Variables

Key global variables include:

crypto – Web‑compatible Crypto API.

fetch() – Browser‑compatible Fetch API.

process – Access to command‑line arguments, stdio, etc.

structuredClone() – Deep‑clone objects.

URL – Browser‑compatible URL handling class.

1.2 Built‑in Node.js Modules

Most Node.js APIs are provided via modules. Common modules include:

node:assert/strict – Assertion functions for testing and runtime checks.

import * as assert from 'node:assert/strict';
assert.equal(3 + 4, 7);
assert.equal('abc'.toUpperCase(), 'ABC');
assert.notEqual({prop: true}, {prop: true}); // shallow compare
assert.deepEqual({prop: true}, {prop: true}); // deep compare

node:child_process – Run commands synchronously or in separate processes.

node:fs – File system operations (read, write, copy, delete).

node:os – OS‑specific constants and utilities.

node:path – Cross‑platform path handling.

node:stream – Stream API specific to Node.js.

node:util – Various utility functions.

Modules can be listed with builtinModules() from node:module:

import * as assert from 'node:assert/strict';
import {builtinModules} from 'node:module';
const modules = builtinModules.filter(m => !m.startsWith('_'));
modules.sort();
assert.deepEqual(
  modules.slice(0, 5),
  [
    'assert',
    'assert/strict',
    'async_hooks',
    'buffer',
    'child_process',
  ]
);

1.3 Function Styles in Node.js

Using node:fs as an example, Node.js provides three function styles:

Synchronous – e.g., fs.readFileSync(path, options?) Callback‑based asynchronous – e.g., fs.readFile(path, options?, callback) Promise‑based asynchronous – e.g., fsPromises.readFile(path, options?) Examples:

try {
  const result = fs.readFileSync('/etc/passwd', {encoding: 'utf-8'});
  console.log(result);
} catch (err) {
  console.error(err);
}
import * as fsPromises from 'node:fs/promises';
try {
  const result = await fsPromises.readFile('/etc/passwd', {encoding: 'utf-8'});
  console.log(result);
} catch (err) {
  console.error(err);
}
fs.readFile('/etc/passwd', {encoding: 'utf-8'}, (err, result) => {
  if (err) {
    console.error(err);
    return;
  }
  console.log(result);
});

Node.js Event Loop

Node.js runs JavaScript on a single main thread that continuously executes an event loop, processing tasks (callbacks) from a queue. Tasks can be added by user code, I/O operations, timers, etc.

The loop repeatedly dequeues a task and executes it:

while (true) {
  const task = taskQueue.dequeue(); // blocks if empty
  task();
}

Key phases include Timers, I/O polling, Check (setImmediate), and others. The loop runs each phase until its queue is empty or a limit is reached.

2.1 Run‑to‑Completion

Each task runs to completion before the next task starts, ensuring no overlapping execution.

2.2 Why Single‑Threaded?

A single thread simplifies shared data handling and reduces overhead compared to spawning many OS threads for each I/O operation.

2.3 Event Loop Phases

Important phases:

Timers – Executes callbacks scheduled by setTimeout and setInterval.

Poll – Processes I/O callbacks.

Check – Executes setImmediate callbacks.

2.4 Next‑Tick and Microtasks

After each task, a sub‑loop runs:

Next‑tick tasks added via process.nextTick().

Microtasks added via queueMicrotask() or resolved Promises.

These run before the next event‑loop phase.

function enqueueTasks() {
  Promise.resolve().then(() => console.log('Promise reaction 1'));
  queueMicrotask(() => console.log('queueMicrotask 1'));
  process.nextTick(() => console.log('nextTick 1'));
  setImmediate(() => console.log('setImmediate 1'));
  setTimeout(() => console.log('setTimeout 1'), 0);

  Promise.resolve().then(() => console.log('Promise reaction 2'));
  queueMicrotask(() => console.log('queueMicrotask 2'));
  process.nextTick(() => console.log('nextTick 2'));
  setImmediate(() => console.log('setImmediate 2'));
  setTimeout(() => console.log('setTimeout 2'), 0);
}
setImmediate(enqueueTasks);

2.5 Task Scheduling Variants

Timers: setTimeout, setInterval Check: setImmediate Next‑tick: process.nextTick Microtasks: queueMicrotask, Promises

2.5.1 Next‑Tick, Microtasks, and Macrotasks

Example demonstrating execution order:

function enqueueTasks() {
  Promise.resolve().then(() => console.log('Promise reaction 1'));
  queueMicrotask(() => console.log('queueMicrotask 1'));
  process.nextTick(() => console.log('nextTick 1'));
  setImmediate(() => console.log('setImmediate 1'));
  setTimeout(() => console.log('setTimeout 1'), 0);

  Promise.resolve().then(() => console.log('Promise reaction 2'));
  queueMicrotask(() => console.log('queueMicrotask 2'));
  process.nextTick(() => console.log('nextTick 2'));
  setImmediate(() => console.log('setImmediate 2'));
  setTimeout(() => console.log('setTimeout 2'), 0);
}
setImmediate(enqueueTasks);

Starving the Event Loop

Recursive next‑tick or microtask queues can prevent the loop from reaching I/O phases, effectively starving the event loop.

import * as fs from 'node:fs/promises';
function timers() { setTimeout(() => timers(), 0); }
function immediate() { setImmediate(() => immediate()); }
function nextTick() { process.nextTick(() => nextTick()); }
function microtasks() { queueMicrotask(() => microtasks()); }

timers();
console.log('AFTER');
console.log(await fs.readFile('./file.txt', 'utf-8'));

Node.js Process Exit

The event loop checks reference counts after each iteration; if zero, Node.js exits. Timers, intervals, and immediates increase the count, while completed tasks decrease it.

function timeout(ms) {
  return new Promise(resolve => {
    setTimeout(resolve, ms);
  });
}
await timeout(3000);

Promises without timers do not keep the process alive.

function foreverPending() {
  return new Promise(() => {});
}
await foreverPending();

Libuv: The Cross‑Platform Asynchronous I/O Library

Libuv handles asynchronous I/O (TCP, UDP, terminal I/O, pipes) using platform‑specific mechanisms (epoll, kqueue, IOCP, etc.) and runs these operations on the main thread.

3.2 How Libuv Handles Blocking I/O

Blocking native APIs (e.g., file I/O, DNS) are executed in a thread pool, keeping the main thread responsive.

3.3 Libuv Beyond I/O

Thread‑pool task execution

Signal handling

High‑resolution timers

Threading and synchronization primitives

Running User Code Outside the Main Thread

To avoid blocking the event loop, heavy computations can be split into chunks using setImmediate or off‑loaded to separate threads or processes.

4.1 Worker Threads

Worker Threads implement a Web Workers‑like API, each with its own event loop, V8 isolate, and global variables. Communication occurs via parentPort and can use SharedArrayBuffer, Atomics, or message passing.

4.2 Clusters

Clusters create multiple Node.js processes that share a server port, allowing load distribution while keeping processes isolated.

4.3 Child Processes

Child processes spawn new OS processes to run native commands, often via a shell.

References

Node.js Event Loop documentation, articles by Daniel Khan, Mark Meyer, videos by Sam Roberts and Bryan Hughes, libuv design overviews, and various resources on JavaScript concurrency and worker threads.

Original Source

Signed-in readers can open the original source through BestHub's protected redirect.

Sign in to view source
Republication Notice

This article has been distilled and summarized from source material, then republished for learning and reference. If you believe it infringes your rights, please contactadmin@besthub.devand we will review it promptly.

Backend DevelopmentNode.jsAsynchronous ProgrammingEvent Looplibuv
Node Underground
Written by

Node Underground

No language is immortal—Node.js isn’t either—but thoughtful reflection is priceless. This underground community for Node.js enthusiasts was started by Taobao’s Front‑End Team (FED) to share our original insights and viewpoints from working with Node.js. Follow us. BTW, we’re hiring.

0 followers
Reader feedback

How this landed with the community

Sign in to like

Rate this article

Was this worth your time?

Sign in to rate
Discussion

0 Comments

Thoughtful readers leave field notes, pushback, and hard-won operational detail here.