What this module covers: Before you write a single line of Node.js, you need a correct mental model of what it actually is. Not "JavaScript on the server" — that's a description, not a model. This module covers the problem Node.js was built to solve, what the V8 engine does, how Node.js differs from browser JavaScript, and how to get your environment set up correctly. By the end you will have Node installed, your first program running, and a clear picture of when Node.js is the right tool — and when it isn't.
The year is 2008. You are building a web application. Your backend is written in Java, PHP, or Ruby. Every time a user makes a request — for a page, for data, for a file upload — your server creates a new thread to handle it.
A thread is expensive. It costs memory (typically 2–8 MB per thread). The operating system has to context-switch between them. Under heavy load, you spawn hundreds of threads. Most of them are not computing anything — they're just waiting. Waiting for the database to return rows. Waiting for a file to be read from disk. Waiting for an external API to respond.
This is the thread-per-request model, and its fundamental problem is that it conflates concurrency with CPU usage. You need concurrent connections. You don't necessarily need concurrent CPU work. Most web requests spend 80–95% of their time on I/O — waiting for something external — not on computation.
Analogy: A thread-per-request server is a waiter who stands frozen at your table until the kitchen finishes cooking; Node.js is the waiter who drops your order at the kitchen window and immediately goes to take the next table's order, circling back only when the food is actually ready.
Ryan Dahl, a software engineer, noticed this in 2009. He was experimenting with non-blocking I/O in C and Lua when he came across Google's V8 JavaScript engine, which Google had just open-sourced as part of Chrome. V8 was fast — dramatically faster than other JavaScript engines at the time. And JavaScript, by its design in the browser, was already event-driven. You attached callbacks to button clicks. You didn't block waiting for a user to click.
His insight: take V8 out of the browser, add APIs for the file system and networking, and you have a runtime where I/O is inherently non-blocking. You never wait. You register a callback and go handle something else.
He presented Node.js at JSConf EU in November 2009. The demo — a web server in 8 lines of JavaScript — caused immediate excitement. Node.js was not just a new language runtime. It was a different way of thinking about concurrency.
1. V8 — Google's JavaScript engine, written in C++. V8 compiles your JavaScript to machine code and executes it. It is the same engine that runs JavaScript in Chrome and Edge.
2. libuv — A C library that handles asynchronous I/O. It provides the event loop, thread pool, and cross-platform abstractions for networking (TCP, UDP), file system, DNS, and more. libuv is what makes Node.js non-blocking — when you read a file or make a network request, libuv handles the OS-level call and notifies Node.js when it's done.
3. The Node.js standard library — A set of built-in JavaScript modules: fs for files, http for servers, path for paths, crypto for cryptography, and many others. These are the Node.js APIs you call in your code.
When you run node server.js, what actually happens:
V8 parses and compiles your JavaScript to machine code
Your code runs on a single thread managed by V8
When your code calls an I/O operation (read a file, open a socket), it hands the work to libuv
libuv queues the operation with the operating system
The event loop continues running — handling other work, timers, callbacks
When the OS signals the I/O is complete, libuv puts the callback into the event queue
The event loop picks it up and runs it
This is the core model. One thread. Non-blocking I/O. Event-driven callbacks. We will go much deeper into the event loop mechanics in Phase 3 — but this mental model is all you need for Phase 1 and Phase 2.
JavaScript runs in two environments: the browser and Node.js. The language itself is the same — the same syntax, the same Array, Map, Promise. But the environment around it is completely different.
Feature
Browser
Node.js
window object
✅
❌
document, DOM
✅
❌
process object
❌
✅
fs (file system)
❌
✅
http (TCP server)
❌
✅
require() / import
❌ (import/export only)
✅ Both (CommonJS + ESM)
fetch
✅
✅ (Node 18+)
localStorage
✅
❌
setTimeout / setInterval
✅
✅
console.log
✅
✅
In the browser, JavaScript's job is to manipulate the DOM and respond to user events. The global object is window. APIs exist to talk to the user's screen.
In Node.js, JavaScript's job is to build servers, process files, and communicate over networks. The global object is global (though in modern code you rarely reference it directly). APIs exist to talk to the operating system.
A note on globalThis: Node.js's global and the browser's window are environment-specific — code written against one won't reference the other. globalThis is the standardised way to access "the global object" that works identically in both environments (and in web workers, Deno, and Bun). If you're writing code that might run in more than one JavaScript environment, reach for globalThis instead of global or window.
This is why code you write for Node.js cannot run in the browser (it uses fs, http, process) and browser-specific code cannot run in Node.js (it uses document, window.location, localStorage).
nvm — Node Version Manager — lets you install and switch between multiple Node.js versions on the same machine. This matters because different projects require different Node.js versions.
# Install nvm (macOS / Linux) — check https://github.com/nvm-sh/nvm for the # current release tag and use that in place of the version below curl -o- https://raw.githubusercontent.com/nvm-sh/nvm/v0.40.3/install.sh | bash
# Restart your terminal, then verify nvm --version
# Install the current LTS version nvm install --lts
# Use it nvm use --lts
# Verify Node.js and npm are installed node --version # e.g. v22.11.0 npm --version # e.g. 10.9.0
On Windows, use nvm-windows — a separate project with a similar interface.
npm is not a separate install — it ships bundled with the Node.js installer (and with nvm install), so once node --version works, npm already works too.
Before you write a single file, try this: type node with no arguments and press Enter.
$ node Welcome to Node.js v22.11.0. Type ".help" for more information. > 1 + 1 2 > console.log('hello') hello undefined > .exit
This is the REPL (Read-Eval-Print Loop) — an interactive shell that reads a line of JavaScript, evaluates it, prints the result, and loops back for the next line. It's the same V8 engine, just without a file behind it. For a lot of developers, this is the literal first moment they encounter Node.js: no project, no package.json, just a prompt where you can try out a snippet of JavaScript. Exit with .exit or Ctrl+D.
Hello from Node.js! Node version: v22.11.0 Platform: darwin Current directory: /Users/jatin/projects
A few things to notice:
process is a global object in Node.js — no import needed. It gives you access to the runtime environment: Node version, OS platform, environment variables, command-line arguments, and more.
process.version is the Node.js version running your program.
process.platform is darwin (macOS), linux, or win32.
process.cwd() is the current working directory — where you ran node from, not where the file lives.
This is where Node.js starts to feel different. Paste this into server.js:
// server.js const http = require('http');
const server = http.createServer((req, res) => { // req = the incoming request // res = the response we will send back
console.log(`${req.method} ${req.url}`);
res.writeHead(200, { 'Content-Type': 'text/plain' }); res.end('Hello from Node.js!\n'); });
server.listen(3000, () => { console.log('Server running at http://localhost:3000/'); console.log('Press Ctrl+C to stop.'); });
Run it:
node server.js
Open http://localhost:3000 in your browser. You will see "Hello from Node.js!" in the browser and GET / logged in your terminal.
Press Ctrl+C to stop the server.
What just happened:
http.createServer() creates a TCP server that listens for HTTP requests
The callback function (req, res) => { ... } runs every time a request arrives
res.writeHead(200, ...) sends the HTTP status code and headers
res.end(...) sends the body and closes the connection
server.listen(3000) starts listening on port 3000
This is the foundation. In practice you will use the Express framework (covered in F-6) which adds routing, middleware, and much more on top of this. But every Express server is ultimately using http.createServer() under the hood.
ReferenceError: undefinedVariable is not defined at Server.<anonymous> (/Users/jatin/projects/server.js:5:11) at node:events:518:28 at node:http:1393:3 at Server.emit (node:events:518:28) at parserOnIncoming (node:_http_server:1061:12) at HTTPParser.parserOnHeadersComplete (node:_http_incoming:76:8)
Breaking this down:
Line 1: The file and line number where the error occurred — server.js:5
The caret ^: Points to the exact position on the line
Error type: ReferenceError — you used a variable that was never declared
Error message: undefinedVariable is not defined
Stack trace: The call stack at the moment of the error, from most recent (your code) to oldest (Node.js internals)
Common error types:
Error Type
Meaning
ReferenceError
Used a variable that doesn't exist
TypeError
Called a method on undefined or null, or wrong argument types
SyntaxError
Invalid JavaScript — missing bracket, typo in keyword
RangeError
Number out of valid range (e.g. invalid array length)
Error
General error — thrown by throw new Error('message')
When you see an error, read from top to bottom: what went wrong, where it went wrong, how it got there. Ignore the Node.js internal stack frames (the lines starting with node:) and focus on your own files.
Node.js excels at tasks where the bottleneck is I/O, not CPU:
✅ API servers and REST backends — Handling thousands of concurrent HTTP connections efficiently. This is the most common use case.
✅ Real-time applications — Chat apps, live notifications, collaborative tools. The event-driven model is a natural fit for WebSockets.
✅ Streaming data — Processing large files or data streams without loading everything into memory at once.
✅ CLI tools and scripts — Node.js is excellent for build tools, automation scripts, and developer tooling. Webpack, ESLint, Prettier, and Vite are all Node.js programs.
✅ Microservices — Lightweight, fast-starting services that communicate over HTTP or message queues.
✅ BFF (Backend for Frontend) — A thin API layer that aggregates data from multiple services for a specific frontend client.
From production: A payments team building a UPI-style webhook ingestion service — the endpoint banks and NPCI call to notify you a payment succeeded or failed — picked Node.js for exactly this reason. At 20,000+ transactions per second, bursty traffic, each incoming request spent roughly 95% of its time waiting on downstream bank and NPCI calls, not computing anything. A thread-per-request Java service handling the same load would have needed thousands of threads sitting idle, parked on socket reads, just to stay ready for the next burst. Node.js handled the same concurrency on a single thread because it was never blocked waiting in the first place.
There are tasks where Node.js is the wrong choice:
❌ CPU-intensive computation — Video encoding, image processing, machine learning inference, cryptographic mining. Node.js runs JavaScript on a single thread. A CPU-heavy operation blocks the event loop and stops everything else from running. (Phase 3 covers techniques for mitigating this — worker threads, native addons — but the baseline is: if your workload is 80% computation and 20% I/O, consider a different runtime.)
❌ Memory-intensive number crunching — Scientific computing, numerical simulations. Languages like Python (NumPy), Julia, or C++ are better suited.
❌ When your team has no JavaScript experience — Using Node.js because it's popular is not a good reason if nobody on your team knows JavaScript well. The wrong tool used by experts beats the right tool used badly.
The decision tree is simple: if your workload is mostly I/O (database queries, file reads, HTTP calls, WebSocket connections), Node.js is an excellent choice. If your workload is mostly CPU, use something better suited.
Node.js = V8 + libuv + standard library. V8 runs your JavaScript. libuv handles all I/O asynchronously. The standard library gives you fs, http, path, and everything else.
Single-threaded, non-blocking. One thread runs your JavaScript. When you do I/O, you don't wait — you register a callback and move on. The event loop calls your callback when the I/O completes.
Node.js is not the browser. No window, no document, no DOM. You have process, fs, http, and the full OS available to you.
Install via nvm.nvm install --lts gives you the stable version. Switch versions per project with nvm use.
Read errors top-to-bottom. Error type → message → your file and line number → ignore Node.js internals.
Best for I/O-heavy workloads. APIs, real-time apps, streaming, CLI tools. Not for CPU-intensive computation.
In the next module we look at the module system — how Node.js files talk to each other, the difference between require() and import, and the module resolution algorithm that determines exactly which file gets loaded when you write require('./utils').
Knowledge Check
Node.js achieves high concurrency primarily through which of the following mechanisms?
Why is Node.js generally considered a poor choice for heavy computational tasks like video encoding or machine learning inference?
When viewing a Node.js error stack trace, what is the most effective way to identify the source of the crash?
Test your knowledge with more question sets
Sign in to access a wider variety of questions and get notified when new practice sets are added to this module.