Part 1: What is Node.js

Understanding the Runtime That Changed Backend Development


The Problem Node.js Solves

In 2009, web servers had a fundamental problem. Apache, the dominant web server, created a new thread or process for every incoming connection. This worked fine for moderate traffic, but at scale, it became a bottleneck.
Consider what happens when a traditional server handles 10,000 concurrent connections:
Traditional Thread-Per-Connection Model: Request 1 --> [Thread 1] --> Database Query (waiting...) --> Response Request 2 --> [Thread 2] --> Database Query (waiting...) --> Response Request 3 --> [Thread 3] --> File Read (waiting...) --> Response ... Request 10000 --> [Thread 10000] --> API Call (waiting...) --> Response Problems: - Each thread consumes ~2MB of memory - 10,000 threads = 20GB of RAM just for thread stacks - Most threads are idle, waiting for I/O - Context switching between threads is expensive - Thread creation/destruction overhead
The insight that led to Node.js was simple but profound: most server operations spend their time waiting. Waiting for databases. Waiting for file systems. Waiting for network responses. Why dedicate an entire thread to doing nothing?

What Node.js Actually Is

Node.js is a JavaScript runtime built on Chrome's V8 JavaScript engine combined with libuv, a cross-platform asynchronous I/O library.
Let's break down each component:

The V8 Engine

V8 is Google's open-source JavaScript engine, written in C++. It compiles JavaScript directly to native machine code instead of interpreting it, making JavaScript execution extremely fast.
javascript
// This JavaScript code: function add(a, b) { return a + b; } // V8 compiles it to native machine code similar to: // mov eax, [a] // add eax, [b] // ret
V8 provides:
  • Just-In-Time (JIT) compilation
  • Garbage collection
  • JavaScript execution
  • Memory management
But V8 alone cannot do I/O operations. In the browser, the browser provides APIs for network requests, timers, and DOM manipulation. Node.js needed something else.

libuv: The Async I/O Foundation

libuv is a C library that provides Node.js with:
  • Asynchronous file system operations
  • Asynchronous DNS resolution
  • Asynchronous TCP and UDP sockets
  • Thread pool for blocking operations
  • Event loop implementation
  • Cross-platform compatibility
Node.js Architecture: ┌─────────────────────────────────────────────────────────────┐ │ Your JavaScript Code │ ├─────────────────────────────────────────────────────────────┤ │ Node.js Bindings (C++) │ │ (fs, http, crypto, etc.) │ ├─────────────────────────────────────────────────────────────┤ │ V8 Engine │ libuv │ │ (JavaScript Execution) │ (Async I/O, Event Loop) │ ├─────────────────────────────────────────────────────────────┤ │ Operating System │ │ (Linux, macOS, Windows) │ └─────────────────────────────────────────────────────────────┘

The Event-Driven, Non-Blocking Model

Node.js uses a single-threaded event loop to handle all requests. Instead of blocking on I/O, Node.js initiates the operation and continues processing other events. When the I/O completes, a callback is executed.
javascript
// Traditional blocking approach (pseudocode): function handleRequest(request, response) { const user = database.query("SELECT * FROM users WHERE id = 1"); // Blocks here const orders = database.query("SELECT * FROM orders WHERE user_id = 1"); // Then blocks here response.send({ user, orders }); } // Node.js non-blocking approach: function handleRequest(request, response) { database.query("SELECT * FROM users WHERE id = 1", (err, user) => { database.query("SELECT * FROM orders WHERE user_id = 1", (err, orders) => { response.send({ user, orders }); }); }); // Execution continues immediately, doesn't wait }
Let's visualize how Node.js handles multiple concurrent requests:
Timeline with Node.js (single thread): Time --> Request 1: [Start] --> [Initiate DB Query] --> [Continue Event Loop] --> [Callback: Send Response] | | ↑ | | | └─── DB working ─────────┴─────────────────────────┘ Request 2: .........[Start] --> [Initiate File Read] --> [Continue] --> [Callback: Send Response] | | ↑ └─── OS reading ─────┴───────────────────┘ Request 3: .................[Start] --> [Initiate API Call] --> [Callback: Send Response] | ↑ └─── Network waiting ─────┘ Single thread handles all three requests without blocking!

Node.js vs Browser JavaScript

While both environments run JavaScript, they have different capabilities and APIs.

What Node.js Has That Browsers Don't

javascript
// File System Access const fs = require('fs'); const content = fs.readFileSync('./file.txt', 'utf8'); // Operating System Information const os = require('os'); console.log(os.cpus()); // CPU information console.log(os.totalmem()); // Total memory console.log(os.homedir()); // Home directory // Process Control console.log(process.pid); // Process ID console.log(process.argv); // Command line arguments console.log(process.env); // Environment variables process.exit(0); // Exit the process // Network Servers const http = require('http'); http.createServer((req, res) => { res.end('Hello World'); }).listen(3000); // Child Processes const { spawn } = require('child_process'); const ls = spawn('ls', ['-la']); // Native Module Loading const crypto = require('crypto'); const path = require('path'); const buffer = Buffer.from('Hello');

What Browsers Have That Node.js Doesn't

javascript
// DOM Manipulation (browser only) document.getElementById('app'); document.querySelector('.class'); element.addEventListener('click', handler); // Window Object (browser only) window.location; window.localStorage; window.alert('Hello'); // Web APIs (browser only) fetch('https://api.example.com'); // Node.js 18+ has fetch, but it's different WebSocket; Service Workers;

What Both Share

javascript
// Core JavaScript const arr = [1, 2, 3].map(x => x * 2); const obj = { ...source, newProp: 'value' }; async function fetchData() { } class MyClass { } // Standard Built-in Objects JSON.parse('{"key": "value"}'); Math.random(); Date.now(); Promise.all([]); new Map(); new Set(); // Console console.log('Debug info'); console.error('Error info'); // Timers setTimeout(() => {}, 1000); setInterval(() => {}, 1000);

When to Use Node.js

Node.js excels in specific scenarios. Understanding these helps you make informed architectural decisions.

Ideal Use Cases

1. I/O-Intensive Applications
Applications that spend most of their time waiting for I/O rather than computing.
javascript
// Example: API Gateway aggregating multiple services async function handleRequest(req, res) { // All these I/O operations can happen concurrently const [users, products, orders] = await Promise.all([ fetch('http://user-service/users'), fetch('http://product-service/products'), fetch('http://order-service/orders') ]); res.json({ users, products, orders }); }
2. Real-Time Applications
WebSockets and event-driven architecture make real-time apps natural.
javascript
const WebSocket = require('ws'); const wss = new WebSocket.Server({ port: 8080 }); const clients = new Set(); wss.on('connection', (ws) => { clients.add(ws); ws.on('message', (message) => { // Broadcast to all connected clients clients.forEach(client => { if (client.readyState === WebSocket.OPEN) { client.send(message); } }); }); ws.on('close', () => clients.delete(ws)); });
3. Streaming Data
Node.js streams are perfect for processing large data without loading it all into memory.
javascript
const fs = require('fs'); const zlib = require('zlib'); // Process a 10GB file without using 10GB of memory fs.createReadStream('large-file.txt') .pipe(zlib.createGzip()) .pipe(fs.createWriteStream('large-file.txt.gz'));
4. Microservices
Lightweight, fast startup, and npm ecosystem make Node.js great for microservices.
javascript
// A microservice can be this simple const express = require('express'); const app = express(); app.get('/health', (req, res) => res.json({ status: 'healthy' })); app.get('/users/:id', async (req, res) => { const user = await userService.findById(req.params.id); res.json(user); }); app.listen(3000);

When Node.js Might Not Be Ideal

1. CPU-Intensive Computations
Heavy computation blocks the event loop, affecting all requests.
javascript
// BAD: This blocks the entire server app.get('/fibonacci/:n', (req, res) => { const n = parseInt(req.params.n); const result = fibonacci(n); // CPU-intensive, blocks event loop res.json({ result }); }); // BETTER: Offload to worker thread const { Worker } = require('worker_threads'); app.get('/fibonacci/:n', (req, res) => { const worker = new Worker('./fibonacci-worker.js', { workerData: { n: parseInt(req.params.n) } }); worker.on('message', (result) => { res.json({ result }); }); });
2. Legacy System Integration
When you need deep integration with Java/.NET ecosystems, using their native languages often makes more sense.

Your First Node.js Program

Let's write code to understand Node.js fundamentals.

Hello World

javascript
// hello.js console.log('Hello, Node.js!'); console.log('Process ID:', process.pid); console.log('Node.js version:', process.version); console.log('Current directory:', process.cwd());
Run it:
node hello.js

Understanding the Global Objects

javascript
// globals.js // 'global' is Node.js's equivalent to 'window' in browsers console.log(global === globalThis); // true // These are available globally without require console.log(__dirname); // Directory of current file console.log(__filename); // Full path of current file // process is global console.log(process.platform); // 'darwin', 'linux', 'win32' console.log(process.arch); // 'x64', 'arm64' // But module, require, exports are NOT truly global // They're injected into each module console.log(typeof require); // 'function' console.log(typeof module); // 'object' console.log(typeof exports); // 'object'

A Simple HTTP Server

javascript
// server.js const http = require('http'); const server = http.createServer((request, response) => { // Log every request console.log(`${request.method} ${request.url}`); // Set response headers response.writeHead(200, { 'Content-Type': 'application/json' }); // Send response body response.end(JSON.stringify({ message: 'Hello from Node.js!', timestamp: new Date().toISOString(), nodeVersion: process.version })); }); const PORT = 3000; server.listen(PORT, () => { console.log(`Server running at http://localhost:${PORT}`); });
Run it and test:
bash
node server.js & curl http://localhost:3000

Demonstrating Non-Blocking I/O

javascript
// non-blocking.js const fs = require('fs'); console.log('1. Starting program'); // This is non-blocking fs.readFile('./package.json', 'utf8', (err, data) => { console.log('3. File read complete'); console.log(' File size:', data.length, 'bytes'); }); console.log('2. Continuing without waiting'); // Output order: 1, 2, 3 // Not: 1, 3, 2
This demonstrates the core Node.js paradigm: we initiate an operation and continue executing without waiting for it to complete.

Understanding the Module System

Node.js wraps every file in a function, providing module-level scope:
javascript
// What you write: const x = 1; module.exports = { x }; // What Node.js actually executes: (function(exports, require, module, __filename, __dirname) { const x = 1; module.exports = { x }; });
This explains why:
  • Variables are file-scoped, not global
  • require, module, exports, __filename, __dirname are available
  • Each file is isolated from others
javascript
// math.js function add(a, b) { return a + b; } function subtract(a, b) { return a - b; } module.exports = { add, subtract }; // main.js const math = require('./math'); console.log(math.add(2, 3)); // 5 console.log(math.subtract(5, 2)); // 3

Summary

Node.js is:
  • A JavaScript runtime built on V8 and libuv
  • Single-threaded with an event loop for concurrency
  • Non-blocking by default for I/O operations
  • Ideal for I/O-intensive, real-time, and streaming applications
  • Not ideal for CPU-intensive computations without worker threads
Key concepts to remember:
  • The event loop is the heart of Node.js
  • Non-blocking I/O allows handling thousands of concurrent connections
  • Everything is asynchronous by default
  • The module system provides encapsulation

Questions to Think About

  1. Why does a single-threaded model work for servers?
Most server operations are I/O-bound, not CPU-bound. The thread spends 99% of its time waiting for databases, file systems, and network responses. Node.js uses this waiting time to handle other requests instead of idling.
  1. How does Node.js achieve concurrency with a single thread?
Through the event loop and asynchronous I/O. When an I/O operation is initiated, Node.js registers a callback and continues processing other events. The operating system handles the actual I/O, and Node.js is notified when it completes.
  1. What happens if you block the event loop?
All requests wait. If you run a CPU-intensive computation for 5 seconds, no other requests can be processed during that time. This is why you should offload heavy computations to worker threads.
  1. How is Node.js different from just running JavaScript?
Node.js provides runtime APIs that JavaScript engines alone don't have: file system access, network capabilities, process control, and more. It also provides the event loop and async I/O infrastructure.
  1. When would you choose Node.js over other technologies?
When your application is I/O-intensive (APIs, real-time apps, streaming), when you want to share code between frontend and backend, when you need fast prototyping with a huge ecosystem, or when you're building microservices that need quick startup times.

Next: Part 2 - Node.js Architecture Deep Dive, where we explore the event loop phases, libuv internals, and how async operations really work.
All Blogs
Tags:nodejsjavascriptv8-engineevent-loopbackend