Node.js Runtime
Node.js is a JavaScript runtime built on Chrome's V8 engine. Its non-blocking, event-driven I/O model lets a single process serve thousands of concurrent connections, making it the foundation of most modern web backends.
What is this?
Node.js executes JavaScript outside the browser using the V8 engine. Instead of one thread per connection, Node runs a single-threaded event loop that hands slow operations (file I/O, network, databases) to the OS or worker threads and continues with other work until the result is ready. The npm registry provides the world's largest ecosystem of packages for building servers, CLIs and tooling.
Why do you need it?
- One Node process handles thousands of concurrent connections using little memory, because it never blocks a thread waiting on I/O.
- JavaScript on the server means one language across frontend and backend, plus shared validation and type definitions.
- npm gives instant access to mature packages for every concern: HTTP, databases, auth, testing and observability.
- The event loop makes async code natural, and streams make it easy to process large payloads incrementally.
Mental model
A single waiter who never waits
The Node.js event loop
A traditional server is one waiter per table who stands around while the kitchen cooks. Node is one waiter who writes all orders on a board (the callback queue), serves other tables while the kitchen works, and only returns when a dish (an I/O result) is ready.
How it works
- When you run node app.js, V8 compiles your JavaScript and the script schedules work on the event loop.
- Async operations (fs.readFile, fetch, database queries) are handed off to libuv's thread pool or the OS, and callbacks are queued when they complete.
- The event loop processes phases in order: timers, pending callbacks, poll (I/O), check (setImmediate), close callbacks.
- While callbacks run, the single JavaScript thread executes them one at a time; there is no preemption between them.
- When the queue is empty and nothing is scheduled, the process exits with code 0 — that is why a bare script ends immediately.
The Node.js event loop: pending I/O is offloaded and results come back as callbacks.
Key commands
node --versionPrint the installed Node.js version.
v22.11.0
npm init -yCreate a package.json with default values in the current directory.
Wrote to package.json: ...
node server.jsRun a JavaScript file with the Node runtime.
npm install expressInstall a package and record it in dependencies.
added 66 packages in 2s
node --watch server.jsRun a script and automatically restart it when files change.
Examples
A minimal HTTP server
const http = require("node:http") const server = http.createServer((req, res) => { res.writeHead(200, { "Content-Type": "application/json" }) res.end(JSON.stringify({ message: "hello", path: req.url }))}) server.listen(3000, () => { console.log("listening on http://localhost:3000")})createServer registers a callback that runs on every request. Node never creates a thread per request — the same callback is invoked by the event loop for each connection. listen binds the server to port 3000. Save it as server.js, run node server.js, and visit localhost:3000.
In production
- API servers and microservices built with Express, Fastify, NestJS or Koa.
- Serverless functions on AWS Lambda, Vercel and Cloudflare run JavaScript via Node runtimes.
- CLI tooling (esbuild, pnpm, Prisma) and build pipelines are themselves Node programs.
- Real-time services use Node's stream and event capabilities for WebSockets and messaging.
Best practices
- Always use the built-in node: prefix (require('node:fs')) to avoid shadowing by third-party modules.
- Handle unhandled promise rejections and uncaught exceptions with process-level handlers.
- Set NODE_ENV=production in deployments so libraries drop debug overhead.
- Pin dependency versions and lockfiles, and run npm audit to surface vulnerabilities.
- Keep worker processes for CPU-bound work; the event loop must never be blocked.
Common mistakes
- Doing CPU-heavy work (JSON.stringify of huge objects, hashing) synchronously and blocking the event loop.
- Calling sync variants (fs.readFileSync) inside request handlers, stalling all other requests.
- Missing .catch() on promises, causing silent failures or unhandled rejections.
- Assuming .env files are loaded automatically — you need a loader or Node's --env-file flag.
Common issues
Error: Cannot find module 'express'The package is not installed, or you are running the file from the wrong directory.
Run npm install <package> and ensure node_modules exists in the same directory as your script.
EADDRINUSE: address already in use :::3000Another process already listens on port 3000.
Stop the other process, or start with a different port (PORT=3001 node server.js).
UnhandledPromiseRejectionWarningA promise rejected and nobody called .catch().
Attach a catch handler or use try/catch inside an async function.
Debugging tips
- console.log is fine for quick checks; use the --inspect flag and Chrome DevTools for stepping.
- node --trace-warnings surfaces the stack of the promise that was rejected but not handled.
- process.memoryUsage() and process.cpuUsage() reveal leaks and blocking work in production.
- Write small node -e one-liners to verify package behavior before integrating it.
Interview checkpoint
Hands-on lab
- Initialize a Node project with npm
- Write and run a minimal HTTP server
- Observe the event loop with async operations
- Debug a port conflict and a missing module
Build and debug a Node HTTP server
30 min · beginner
Quick quiz
Which of these is the correct way to import the built-in filesystem module?
Node.js creates a new OS thread for every incoming HTTP request.
Why does a simple Node script sometimes exit before your async work finishes?
Cheat sheet
node app.jsRun a scriptnode --watch app.jsAuto-restart on changenode --env-file=.env app.jsLoad env vars (Node 20+)node --inspect app.jsOpen the debuggernpm init -yCreate package.jsonnpm install <pkg>Add a dependencynpm install -D <pkg>Add a dev dependencynpm audit fixPatch known vulnerabilities