What is Node.js and how does it work?
Node.js - a JavaScript runtime that executes JS code outside the browser, on the server.
Theory
TL;DR
- Node.js is not a framework or a language - it's a runtime environment for JavaScript
- Built on V8 (Chrome's JS engine) + libuv (a C++ async I/O library)
- Single JS thread + non-blocking I/O: your code never waits idle for file reads or network calls
- Great for I/O-heavy work (APIs, real-time apps, streaming); bad fit for heavy CPU computation
- npm ecosystem: over 2 million packages
Quick example
const http = require('http');
const server = http.createServer((req, res) => {
res.writeHead(200, { 'Content-Type': 'text/plain' });
res.end('Hello from Node.js');
});
server.listen(3000, () => {
console.log('Listening on port 3000');
});An HTTP server in 10 lines. No Apache, no Nginx config. Node.js handles all the networking through its built-in http module.
How Node.js handles a request
The JS thread is single. One execution context at a time. So what happens when 1,000 users hit your server at once?
Node.js delegates blocking work - file reads, database queries, network calls - to libuv. libuv maintains a thread pool (4 threads by default) that performs actual I/O at the OS level. When the work finishes, libuv puts the callback into the event queue. The event loop picks it up and runs it on the JS thread.
Your JS Code
|
Node.js APIs (fs, http, net, crypto...)
|
libuv (event loop + thread pool)
|
OS (file system, network, timers)The JS thread stays free the whole time the OS is doing actual work. That's how Node.js handles thousands of concurrent connections on one thread - most of the time it's just waiting for I/O, not computing.
V8 and libuv
V8 compiles JavaScript to native machine code. Not interpreted, not run through a slow bytecode layer - compiled. That's why Node.js is fast for JS execution.
libuv is the other half. It's a C++ library originally written for Node.js, now used by other projects as well. It provides:
- The event loop
- Async file I/O (thread pool, 4 by default, configurable via
UV_THREADPOOL_SIZE) - TCP/UDP sockets
- DNS resolution
- Timers (
setTimeout,setInterval)
Without libuv, Node.js would just be V8 with no way to communicate with the OS asynchronously.
When Node.js struggles
The single JS thread is a real constraint. Run a CPU-heavy operation - parsing a large JSON, image processing, an ML inference call - and the event loop blocks. Every other request waits.
// This freezes the event loop for all users while it runs
app.get('/report', (req, res) => {
const result = generateHeavyReport(); // blocks the JS thread
res.json({ result });
});For CPU-intensive work, use Worker Threads (available since Node 12) or offload to a separate service. Node.js works well as an API gateway and coordinator, not as a computation engine.
Where Node.js fits
- REST APIs and GraphQL servers
- WebSocket servers and real-time apps (chat, live updates, collaborative tools)
- CLI tools and build scripts (webpack, Vite, ESLint all run on Node)
- Serverless functions (AWS Lambda, Vercel, Cloudflare Workers)
- Microservices that mostly do I/O
The npm ecosystem is part of the value. Over 2 million packages means most problems already have a published solution.
Common mistakes
Blocking the event loop with sync operations
// Bad - blocks the entire server while reading
const data = fs.readFileSync('./large-file.json');
// Good - libuv handles it, JS thread stays free
fs.readFile('./large-file.json', (err, data) => {
// runs when ready
});Assuming Node.js is multi-threaded
Node.js has one JS thread. setTimeout and setInterval don't run in parallel - they schedule callbacks. If the event loop is blocked, timers fire late.
Using Node.js for CPU tasks without Worker Threads
One CPU-bound request can slow down every other user's response. If you need heavy CPU work, use worker_threads or a child process.
Ignoring unhandled promise rejections
Since Node 15, an unhandled rejection crashes the process by default. Always add .catch() or use try/catch in async functions.
// Crashes the process in Node 15+
fetchUserData(userId); // async function, no error handling attachedReal-world usage
- Express, Fastify, Koa - HTTP frameworks built on top of Node's
httpmodule - Next.js - SSR and API routes run in a Node.js process
- Socket.io - real-time communication over WebSocket
- NestJS - structured backend framework for larger applications
- Tooling: Vite, webpack, TypeScript compiler, ESLint, Prettier
I've seen teams pick Node.js for a data pipeline that was mostly waiting on database queries. It handled the concurrency without any additional infrastructure.
Follow-up questions
Q: What is the difference between Node.js and a browser's JavaScript engine?
A: Both can use V8, but Node.js adds libuv for OS access (file system, networking) and has no DOM, window, or browser APIs. The browser has document, fetch, localStorage. Node has fs, net, child_process.
Q: Why is Node.js single-threaded but still handles many connections?
A: JS execution is single-threaded, but I/O is handled by libuv's thread pool and OS-level async calls. Node.js spends most of its time waiting for I/O, not computing. During that wait, the event loop picks up other callbacks.
Q: What happens when the event loop is blocked?
A: All pending requests wait. If the JS thread runs a 5-second computation, every response is delayed by those 5 seconds. This is why Node.js is not suited for CPU-intensive tasks without Worker Threads.
Q: How many threads does Node.js actually use?
A: One JS thread. libuv adds 4 pool threads by default (configurable via UV_THREADPOOL_SIZE). Network I/O uses OS-level async and doesn't go through the pool. A typical Node process shows 6-8 threads in top or htop.
Q: Is libuv specific to Node.js?
A: No. libuv is an independent open-source C++ library (libuv/libuv on GitHub). Node.js was the original reason it was created, but other runtimes and tools use it too.
Examples
Basic HTTP server
const http = require('http');
const server = http.createServer((req, res) => {
if (req.url === '/health') {
res.writeHead(200, { 'Content-Type': 'application/json' });
res.end(JSON.stringify({ status: 'ok' }));
return;
}
res.writeHead(404);
res.end('Not found');
});
server.listen(3000, () => console.log('Server on port 3000'));No dependencies. Node's http module handles TCP connections, HTTP parsing, and keeps the server alive.
Async file read with error handling
const fs = require('fs').promises;
async function loadConfig(path) {
try {
const raw = await fs.readFile(path, 'utf8');
return JSON.parse(raw);
} catch (err) {
console.error('Config load failed:', err.message);
return null;
}
}
loadConfig('./config.json').then(config => {
console.log('App name:', config?.name);
});fs.promises.readFile hands the work to libuv's thread pool. The JS thread stays free until the file is ready. The await pauses only this async function, not the whole event loop.
Event loop execution order
console.log('1 - sync');
setTimeout(() => console.log('3 - macrotask'), 0);
Promise.resolve().then(() => console.log('2 - microtask'));
console.log('4 - sync');
// Output:
// 1 - sync
// 4 - sync
// 2 - microtask (Promise callback)
// 3 - macrotask (setTimeout)Microtasks (Promise callbacks) run before macrotasks (setTimeout), even with a 0ms delay. This surprises developers who expect setTimeout(..., 0) to run right after the current synchronous block.
Short Answer
Interview readyA concise answer to help you respond confidently on this topic during an interview.