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.

Introduction to Node.js

What is Node.js?

Node.js is an open-source, cross-platform JavaScript runtime environment that executes JavaScript code outside a web browser. It allows developers to use JavaScript to write command-line tools and for server-side scripting—running scripts server-side to produce dynamic web page content before the page is sent to the user’s web browser. Think of it this way: JavaScript was born inside the browser, confined to making web pages interactive. Node.js broke it out of that cage. Ryan Dahl looked at how web servers handled connections in 2009—most used a “one thread per connection” model that buckled under load—and realized that JavaScript’s event-driven nature was actually perfect for building servers that could handle thousands of simultaneous connections without breaking a sweat.
Node.js was created by Ryan Dahl in 2009 and has since become one of the most popular backend technologies, powering companies like Netflix, PayPal, LinkedIn, and Uber. PayPal famously rewrote their Java backend in Node.js and saw a 35% decrease in average response time and double the number of requests handled per second—with fewer people on the team.

Key Features

FeatureDescription
Asynchronous & Event-DrivenAll APIs are non-blocking. The server never waits for an API to return data.
Single-ThreadedUses event looping for high scalability without thread management overhead.
Fast ExecutionBuilt on Chrome’s V8 engine, compiling JavaScript directly to machine code.
No BufferingOutputs data in chunks, enabling efficient streaming.
Rich EcosystemOver 2 million packages available via NPM.
Cross-PlatformRuns on Windows, macOS, Linux, and more.

When to Use Node.js

Ideal for:
  • Real-time applications (chat, gaming, collaboration tools)
  • API servers and microservices
  • Streaming applications
  • Single Page Application (SPA) backends
  • CLI tools and scripts
  • IoT applications
Not ideal for:
  • CPU-intensive computations (video encoding, machine learning)
  • Applications requiring multi-threading by design
  • Monolithic enterprise applications with heavy business logic

Node.js vs Other Backend Runtimes

DimensionNode.jsPython (Django/Flask)GoJava (Spring)
Concurrency modelSingle-threaded event loopMulti-threaded (GIL limits CPU)Goroutines (lightweight threads)OS threads with thread pool
I/O throughputExcellent — non-blocking by defaultGood with async (ASGI), limited in sync modeExcellent — goroutines are cheapGood with reactive (WebFlux)
CPU-bound workPoor without Worker ThreadsModerate (C extensions help)Excellent — compiled, multi-coreExcellent — JIT compiled
Cold start time~50-100ms~200-500ms~5-10ms~1-5 seconds
Memory per instance~30-50MB baseline~40-80MB baseline~5-15MB baseline~100-200MB baseline
Ecosystem size2M+ NPM packages400K+ PyPI packages1M+ Go modules500K+ Maven artifacts
Best fitI/O-heavy APIs, real-timeData science, scripting, web appsSystems, CLIs, high-perf APIsEnterprise, Android, large teams
Decision framework — when to pick Node.js over alternatives:
  1. Your team already knows JavaScript and you want a single language across frontend and backend.
  2. Your workload is I/O-bound (database queries, HTTP calls, file operations) rather than CPU-bound.
  3. You need real-time features (WebSockets, SSE) and want them as first-class citizens.
  4. Startup time matters — serverless functions, CLI tools, or dev tooling.
Pick something else when your workload is primarily CPU-bound computation, when you need strict type safety at compile time (consider Go or Java), or when your team has deep expertise in another ecosystem.

The V8 Engine

At the core of Node.js is the V8 JavaScript engine, the same engine that powers Google Chrome. V8 compiles JavaScript directly to native machine code using Just-In-Time (JIT) compilation, resulting in exceptional performance.

How V8 Works

JavaScript Code → Parser → Abstract Syntax Tree (AST) → Interpreter (Ignition)

                                              Hot Code Optimization (TurboFan)

                                                    Machine Code Execution

Key V8 Optimizations

  1. Hidden Classes: V8 creates hidden classes for objects to optimize property access. This is why initializing all object properties in the constructor (rather than adding them dynamically later) leads to faster code—V8 can share the same hidden class across all instances.
  2. Inline Caching: Remembers where to find object properties so repeated access is nearly as fast as C++ struct access.
  3. Garbage Collection: Automatic memory management with generational GC. Most objects are short-lived, so V8 uses a “nursery” (young generation) that is collected quickly and cheaply. Long-lived objects get promoted to the “old generation” which is collected less frequently. Understanding this matters when debugging memory leaks—tools like --inspect and Chrome DevTools heap snapshots let you see which objects are being retained.

V8 Performance: What the Numbers Look Like

These are rough benchmarks to build intuition, not absolute truths (hardware, Node version, and workload all matter):
OperationApproximate ThroughputNotes
Property access (monomorphic)~1 billion/secV8 optimizes single-shape objects aggressively
Property access (megamorphic)~50-100 million/secObjects with many shapes defeat inline caching
JSON.parse (1KB payload)~500,000/secFaster than manual parsing for structured data
JSON.stringify (1KB object)~300,000/secMajor cost in high-throughput APIs — consider caching
Object creation (small)~100 million/secYoung generation GC handles short-lived objects efficiently
RegExp match (simple)~10-50 million/secCompiled to native code after warmup
Edge case to watch for: Repeatedly creating objects with different property orders (e.g., {a:1, b:2} vs {b:2, a:1}) forces V8 to create separate hidden classes for each shape, which defeats inline caching and can cause 10-20x slowdowns in hot loops. This is why database rows returned from ORMs are usually fast to access — they all share the same shape.

Installation

To get started, you need to install Node.js on your machine.

Windows / macOS / Linux

  1. Visit the official Node.js website.
  2. Download the LTS (Long Term Support) version. This version is recommended for most users as it’s the most stable.
  3. Run the installer and follow the on-screen instructions.

Verifying Installation

Open your terminal or command prompt and run the following commands:
node -v
npm -v
You should see the version numbers for both Node.js and npm (Node Package Manager).

The REPL (Read-Eval-Print Loop)

Node.js comes with a built-in REPL environment. It allows you to execute JavaScript code directly in the terminal. To enter the REPL, simply type node in your terminal:
$ node
> console.log("Hello from Node.js!");
Hello from Node.js!
undefined
> 1 + 1
2
Press Ctrl + C twice to exit the REPL.

Your First Node.js Script

Let’s create a simple file to run with Node.js.
  1. Create a file named app.js.
  2. Add the following code:
const message = "Hello, World!";
console.log(message);

const add = (a, b) => a + b;
console.log("2 + 3 =", add(2, 3));
  1. Run the file using the node command:
node app.js
Output:
Hello, World!
2 + 3 = 5

Global Objects

In the browser, the global object is window. In Node.js, the global object is global.
console.log(global);

// Common globals
console.log(__dirname); // Path to current directory
console.log(__filename); // Path to current file
Note that DOM-related objects like document or window do not exist in Node.js.

The Event Loop

Understanding the Event Loop is crucial to mastering Node.js. It’s the mechanism that allows Node.js to perform non-blocking I/O operations despite JavaScript being single-threaded. The Restaurant Host Analogy: Imagine a restaurant with a single host (the event loop). When a guest arrives, the host doesn’t cook the meal personally—they seat the guest, hand the order to the kitchen (the OS or thread pool), and immediately greet the next guest. When the kitchen rings the bell (a callback), the host delivers the food. One host can manage dozens of tables this way. A traditional multi-threaded server is like hiring a personal waiter for every single guest—it works, but you run out of waiters fast.

Event Loop Phases

   ┌───────────────────────────┐
┌─>│           timers          │ ← setTimeout, setInterval
│  └─────────────┬─────────────┘
│  ┌─────────────┴─────────────┐
│  │     pending callbacks     │ ← I/O callbacks deferred
│  └─────────────┬─────────────┘
│  ┌─────────────┴─────────────┐
│  │       idle, prepare       │ ← internal use only
│  └─────────────┬─────────────┘      ┌───────────────┐
│  ┌─────────────┴─────────────┐      │   incoming    │
│  │           poll            │<─────┤  connections, │
│  └─────────────┬─────────────┘      │   data, etc.  │
│  ┌─────────────┴─────────────┐      └───────────────┘
│  │           check           │ ← setImmediate
│  └─────────────┬─────────────┘
│  ┌─────────────┴─────────────┐
└──┤      close callbacks      │ ← socket.on('close')
   └───────────────────────────┘

Async Scheduling: Decision Framework

Choosing between setTimeout, setImmediate, process.nextTick, and queueMicrotask is a common source of confusion. Use this decision table:
MechanismWhen it runsUse when you need to…Watch out for
process.nextTick()Before any I/O, before promisesEnsure callback runs before event loop continues (e.g., emitting events after constructor returns)Recursive calls starve I/O indefinitely
queueMicrotask()After nextTick, before I/OStandard microtask scheduling (same as resolved promises)Same starvation risk as nextTick
setTimeout(fn, 0)Next iteration, timers phaseDefer work to a future event loop tick with minimum 1ms delayMinimum delay is 1ms (not 0), clamped to 4ms after 5 nested calls in browsers
setImmediate()Current iteration, check phase (after I/O poll)Yield to I/O between iterations of a loopOnly available in Node.js, not browsers
Edge case: Inside an I/O callback (e.g., inside fs.readFile), setImmediate always fires before setTimeout(fn, 0). Outside I/O callbacks, their order is non-deterministic and depends on the event loop’s timing. If your code relies on a specific order between these two, your code has a bug.

Example: Understanding Execution Order

// This example reveals the priority order of Node.js async mechanisms.
// Synchronous code ALWAYS runs first, then the microtask queues, then the event loop phases.

console.log('1: Start');  // Synchronous -- runs immediately

setTimeout(() => {
  console.log('2: Timeout callback');  // Timers phase -- queued in the event loop
}, 0);  // Even with 0ms delay, this waits for the next event loop iteration

setImmediate(() => {
  console.log('3: Immediate callback');  // Check phase -- runs after I/O polling
});

process.nextTick(() => {
  console.log('4: Next tick callback');  // Microtask -- runs BEFORE the event loop continues
});

Promise.resolve().then(() => {
  console.log('5: Promise resolved');  // Microtask -- runs after nextTick, before event loop
});

console.log('6: End');  // Synchronous -- runs immediately

// Output:
// 1: Start
// 6: End
// 4: Next tick callback   (nextTick queue drains first)
// 5: Promise resolved     (promise microtasks drain second)
// 2: Timeout callback     (timers phase -- order with setImmediate varies outside I/O)
// 3: Immediate callback   (check phase)
process.nextTick() callbacks are processed before the event loop continues. Excessive use can starve I/O operations! This is a real production pitfall: if you recursively call process.nextTick(), your I/O callbacks (database responses, HTTP requests) will never execute. The event loop is stuck serving nextTick callbacks forever. If you need to defer work, prefer setImmediate() instead—it yields to the event loop between calls.

The Process Object

The process object is a global that provides information about, and control over, the current Node.js process.

Common Process Properties

// Environment variables
console.log(process.env.NODE_ENV);  // 'development' or 'production'
console.log(process.env.PATH);       // System PATH

// Process info
console.log(process.pid);            // Process ID
console.log(process.ppid);           // Parent process ID
console.log(process.version);        // Node.js version
console.log(process.versions);       // All dependency versions
console.log(process.platform);       // 'darwin', 'win32', 'linux'
console.log(process.arch);           // 'x64', 'arm64', etc.

// Command line arguments
console.log(process.argv);           // Array of arguments
// node app.js --port=3000
// ['path/to/node', 'path/to/app.js', '--port=3000']

// Current working directory
console.log(process.cwd());

// Memory usage
console.log(process.memoryUsage());
// { rss: 30093312, heapTotal: 6537216, heapUsed: 4195024, external: 8272 }

Process Events

// Handle uncaught exceptions
process.on('uncaughtException', (err) => {
  console.error('Uncaught Exception:', err);
  process.exit(1);
});

// Handle unhandled promise rejections
process.on('unhandledRejection', (reason, promise) => {
  console.error('Unhandled Rejection at:', promise, 'reason:', reason);
});

// Graceful shutdown
process.on('SIGTERM', () => {
  console.log('SIGTERM received. Shutting down gracefully...');
  server.close(() => {
    console.log('Process terminated');
    process.exit(0);
  });
});

// Before exit
process.on('beforeExit', (code) => {
  console.log('Process beforeExit event with code:', code);
});

Advanced Setup Options

Using NVM (Node Version Manager)

Managing multiple Node.js versions is essential for professional development. macOS/Linux:
# Install NVM
curl -o- https://raw.githubusercontent.com/nvm-sh/nvm/v0.39.0/install.sh | bash

# Install Node versions
nvm install 20      # Install Node 20
nvm install 18      # Install Node 18
nvm use 20          # Switch to Node 20
nvm alias default 20 # Set default version
nvm list            # List installed versions
Windows (nvm-windows):
# Download from https://github.com/coreybutler/nvm-windows
nvm install 20.0.0
nvm use 20.0.0

Project Structure Best Practices

my-node-app/
├── src/
│   ├── config/         # Configuration files
│   ├── controllers/    # Route controllers
│   ├── middleware/     # Custom middleware
│   ├── models/         # Database models
│   ├── routes/         # Route definitions
│   ├── services/       # Business logic
│   ├── utils/          # Utility functions
│   └── app.js          # Express app setup
├── tests/              # Test files
├── .env                # Environment variables
├── .env.example        # Environment template
├── .gitignore
├── package.json
└── README.md

Summary

  • Node.js is a JavaScript runtime built on Chrome’s V8 engine
  • The Event Loop enables non-blocking I/O with a single thread
  • The process object provides control over the Node.js process
  • Use NVM to manage multiple Node.js versions
  • Understand the Event Loop phases for better async code
  • Follow consistent project structure patterns
Practical tip — avoid blocking the event loop: The single biggest mistake new Node.js developers make is running CPU-intensive work (parsing large JSON, image processing, complex calculations) directly in the main thread. Since there is only one event loop, a 500ms computation means every other user waits 500ms. For CPU-heavy work, use Worker Threads (covered in Chapter 21) or offload to a separate service.

Interview Deep-Dive

Strong Answer:The event loop has six phases that run in a fixed cycle: timers, pending callbacks, idle/prepare, poll, check, and close callbacks.
  • Timers phase executes callbacks scheduled by setTimeout and setInterval whose threshold has elapsed.
  • Pending callbacks phase handles deferred I/O callbacks from the previous cycle (e.g., TCP errors).
  • Poll phase retrieves new I/O events and executes their callbacks. If there is nothing else scheduled, the loop will block here waiting for new I/O.
  • Check phase executes setImmediate callbacks — this is the key distinction.
Inside an I/O callback (such as inside fs.readFile), setImmediate always fires before setTimeout(fn, 0) because after the poll phase completes, the check phase runs next, while setTimeout(fn, 0) must wait for the next iteration to reach the timers phase.Outside an I/O callback, the order between setTimeout(fn, 0) and setImmediate is non-deterministic because it depends on whether the event loop has entered the timers phase before the 1ms minimum delay of setTimeout has elapsed. If your application logic depends on a specific order between these two outside I/O, that is a bug.Before any phase runs, Node.js drains the microtask queue: first all process.nextTick callbacks, then all resolved promise callbacks. This means process.nextTick and queueMicrotask always execute before the event loop moves to its next phase — and recursive nextTick calls can starve I/O indefinitely.In production, I have seen a recursive process.nextTick pattern completely freeze a server’s ability to process incoming HTTP requests. We switched to setImmediate for the recursive callback and the issue resolved immediately because setImmediate yields to the event loop between calls.Follow-up: You mentioned process.nextTick can starve I/O. How would you detect that starvation is happening in a running production server?You would monitor event loop lag — the difference between when a timer was scheduled to fire and when it actually fires. Libraries like monitorEventLoopDelay (built into Node.js perf_hooks since v11) or packages like event-loop-stats expose this metric. If your event loop lag spikes from sub-millisecond to hundreds of milliseconds, something is blocking or starving the loop. You can also use --prof to generate a V8 CPU profile and inspect where time is being spent. In practice, we set up a setInterval health check that logs a timestamp every second; if gaps appear in the log, the event loop was blocked during that window.
Strong Answer:V8 internally assigns a “hidden class” (also called a “shape” or “map”) to every JavaScript object based on its property layout — the names, order, and types of properties. When two objects have the same hidden class, V8 can use inline caching to make property access nearly as fast as a C struct lookup. Instead of doing a hash table lookup every time, V8 remembers the exact memory offset for a property at a given call site.There are three states of inline caching: monomorphic (one shape seen at this call site — fastest), polymorphic (2-4 shapes — still reasonably fast), and megamorphic (many shapes — falls back to a slow generic lookup).Developers defeat this optimization in several ways:
  • Creating objects with properties in different orders: {a:1, b:2} and {b:2, a:1} have different hidden classes even though they look identical.
  • Adding properties dynamically after construction instead of initializing all properties in the constructor or object literal.
  • Deleting properties from objects, which forces V8 to create new hidden classes or fall back to dictionary mode.
  • Passing objects of different shapes through the same function in a hot loop, making call sites megamorphic.
A real-world example: an API server that processes database rows. If the ORM returns all rows with the same property order, V8 creates one hidden class and everything is fast. But if you manually build response objects with conditional properties (if (includeEmail) obj.email = ...), each unique combination gets a different hidden class, and serialization in JSON.stringify slows down significantly for high-throughput endpoints. The fix is to always initialize all properties (even to undefined) in a consistent order.Follow-up: How would you identify that megamorphic call sites are causing a performance problem in your application?Run your app with node --trace-ic app.js to get inline cache state logs, or use --prof followed by node --prof-process to generate a readable CPU profile. You will see functions marked as “megamorphic” in the deopt logs. Chrome DevTools connected via --inspect also shows this in the Performance panel’s bottom-up view. The V8 blog and tools like v8-deopt-viewer can visualize optimization and deoptimization events per function.
Strong Answer:Low CPU with high latency or 503s almost always means the event loop is blocked or starved — something synchronous is hogging the single thread, or the server is running out of some non-CPU resource like file descriptors, database connections, or memory.My debugging approach, in order:
  1. Check event loop lag. Use perf_hooks.monitorEventLoopDelay() or a lightweight interval check. If lag is in the hundreds of milliseconds, something is blocking the loop.
  2. Check connection pools. If the database pool is exhausted (all connections are checked out and waiting), incoming requests queue up and eventually timeout. I would check the pool’s active/waiting count. Same for Redis connections.
  3. Check for file descriptor exhaustion. Each open socket, file handle, and pipe consumes a file descriptor. On Linux, ulimit -n shows the limit (often 1024 by default). Under load, you can hit this silently and new connections fail. lsof -p <pid> | wc -l tells you the current count.
  4. Check for DNS resolution delays. If every outbound request triggers a DNS lookup and the DNS server is slow, you get cascading latency. This is a common surprise in containerized environments.
  5. Take a CPU profile. Connect Chrome DevTools via --inspect, record a profile under load, and look for synchronous functions consuming disproportionate time — often JSON.parse on large payloads, synchronous file reads, or regex backtracking.
  6. Check memory. If the process is near the heap limit, V8 garbage collection pauses can freeze the event loop for hundreds of milliseconds. process.memoryUsage() or a monitoring tool like Prometheus with prom-client will reveal this.
In one production incident, our API returned 503s at 200 requests/second despite 10% CPU. The root cause was a Mongoose query that forgot .lean() on a large result set, causing Mongoose to hydrate 5,000 full document objects per request. The GC pressure from creating and discarding those objects caused event loop stalls. Adding .lean() dropped response time from 800ms to 40ms.Follow-up: How do you instrument a Node.js service so these problems surface in monitoring before users complain?Export event loop lag, heap usage, active handles/requests, and connection pool saturation as Prometheus metrics using prom-client. Set alerts on event loop lag exceeding 100ms, heap usage above 80%, and connection pool wait time. For the event loop specifically, Node.js has process._getActiveHandles().length and process._getActiveRequests().length which reveal resource leaks. Combine with distributed tracing (OpenTelemetry) so you can see which specific code path is slow.
Strong Answer:All three schedule callbacks that run before the event loop continues to the next phase, but they differ in priority and semantics:
  • process.nextTick runs first — before any microtask. Node.js drains the entire nextTick queue before moving on. It exists because early Node.js needed a way to emit events after a constructor returns but before any I/O, ensuring listeners registered synchronously would be called. The classic use case: EventEmitter subclasses that need to emit an event in the constructor. If you emit synchronously, no listeners are attached yet. If you use nextTick, listeners can be attached on the same tick before the emission runs.
  • queueMicrotask runs after nextTick callbacks, in the standard microtask queue (the same queue that resolved promises use). It was added to Node.js to align with the browser-standard microtask API. Use it when you need standard microtask timing without the Node-specific priority of nextTick.
  • Promise.resolve().then() also queues into the microtask queue, effectively the same timing as queueMicrotask. The difference is purely ergonomic — queueMicrotask is a direct API call without creating a throwaway Promise object.
The execution order within a single tick is always: synchronous code, then all nextTick callbacks (recursively), then all microtasks (promises and queueMicrotask), then the event loop proceeds to the next phase.The practical guidance: use process.nextTick only when you specifically need to run before pending microtasks (the EventEmitter constructor pattern). For everything else, prefer queueMicrotask or just use promises naturally. Never use recursive process.nextTick — it starves I/O because Node.js drains the entire nextTick queue before checking for I/O. Recursive setImmediate is the safe alternative because it yields to the event loop between calls.Follow-up: Can you give an example where using process.nextTick instead of setImmediate caused a real production issue?A common pattern is a streaming parser that processes incoming data and emits parsed events. If the parser uses process.nextTick to emit each parsed chunk, and the input arrives faster than the consumer can process, the nextTick queue fills up and I/O callbacks (including the ‘data’ events that bring in new chunks) never execute. The process appears frozen even though it is working — just working on nextTick callbacks forever. The fix is to use setImmediate so each iteration yields to the event loop, allowing I/O to interleave with processing. This exact issue has appeared in popular libraries like JSONStream in certain edge cases.