What is Node.js?
Node.js is an open-source JavaScript runtime that lets you run JavaScript outside the browser — on servers, in containers, and on your own machine as command-line tools. It pairs Google’s high-performance V8 engine with a small C++ core (libuv) that provides non-blocking I/O, so a single Node process can handle thousands of concurrent connections without spawning a thread per request. That combination is why Node powers a huge share of today’s APIs, build tooling, and real-time applications.
Where Node.js came from
Node.js was created by Ryan Dahl in 2009. At the time, most web servers handled each connection with a dedicated thread or process, which scaled poorly under many slow, concurrent clients. Dahl’s insight was to take the same V8 engine that made JavaScript fast in Chrome and wrap it in an event loop that never blocks on I/O. Instead of waiting for a database query or file read to finish, Node registers a callback and moves on, processing results as they arrive.
The project grew rapidly, gained the npm package registry, and is now stewarded by the OpenJS Foundation with a predictable release schedule. Modern versions (Node 20 and 22 LTS) ship with native ECMAScript modules, a built-in fetch, a stable test runner, and a node: prefix for core modules.
The non-blocking, event-driven model
The heart of Node is the event loop: a single-threaded loop that picks up completed I/O events and runs their callbacks. Because I/O is offloaded to the operating system (or a small background thread pool in libuv), the main thread stays free to keep accepting work. You write code that looks sequential with async/await, but under the hood Node is juggling many operations at once.
import { readFile } from "node:fs/promises";
console.log("1. Start");
const data = await readFile("config.json", "utf8");
console.log("3. File contents:", data.trim());
console.log("2. This logs before the file in callback style");
The synchronous console.log calls run immediately, while readFile returns a promise that resolves later. With await the function pauses at line 5 without blocking the rest of the runtime — other timers, sockets, and requests keep being serviced.
Tip: “Single-threaded” refers to your JavaScript callbacks running on one thread. Network and file I/O happen in the background, which is exactly why Node excels at I/O-heavy workloads but should hand off CPU-bound work to worker threads or separate processes.
A minimal HTTP server
Because non-blocking I/O is built in, a fully functional web server fits in a few lines using the core node:http module — no framework required.
import { createServer } from "node:http";
const server = createServer((req, res) => {
res.writeHead(200, { "Content-Type": "application/json" });
res.end(JSON.stringify({ message: "Hello from Node.js", url: req.url }));
});
server.listen(3000, () => {
console.log("Server running at http://localhost:3000");
});
Run it and request the endpoint:
node server.js
curl http://localhost:3000/status
Output:
Server running at http://localhost:3000
{"message":"Hello from Node.js","url":"/status"}
Common use cases
Node’s strengths cluster around tasks that spend most of their time waiting on I/O rather than crunching numbers.
| Use case | Why Node fits |
|---|---|
| REST and GraphQL APIs | Lightweight, fast JSON handling and a vast package ecosystem |
| Real-time apps | WebSockets and the event loop handle many open connections cheaply |
| CLI tools and scripts | One runtime for both your app and your tooling |
| Build tooling | Bundlers, linters, and test runners (Vite, ESLint, Prettier) run on Node |
| Backends-for-frontends | Share types and logic between server and browser JavaScript |
How Node differs from browser JavaScript
Both environments run the same JavaScript language on V8, but the surrounding APIs differ because the goals differ. The browser exposes the DOM, window, and localStorage; Node exposes the file system, processes, networking, and operating-system details.
| Browser JavaScript | Node.js |
|---|---|
window, document, DOM APIs | process, Buffer, globalThis |
| No direct file system access | node:fs for reading and writing files |
Modules via <script type="module"> | ES modules (import) and CommonJS (require) |
| Sandboxed by the browser | Full OS access (files, env vars, child processes) |
fetch, localStorage, alert | fetch (built in since Node 18), no DOM |
ES modules are the modern default; opt in with "type": "module" in package.json or a .mjs extension. CommonJS (require/module.exports) remains common in older packages, and Node interoperates with both.
// ES module (modern default)
import { hostname } from "node:os";
// CommonJS (still widely used)
const { hostname } = require("node:os");
Best practices
- Target an active LTS release (Node 20 or 22) for production — LTS lines get long-term security and bug fixes.
- Prefer ES modules and
async/awaitfor new projects; they are the modern, framework-agnostic default. - Use the
node:prefix (node:fs,node:http) for core modules so they are unmistakable from npm packages. - Keep the event loop free: move CPU-bound work to worker threads or a separate service.
- Reach for the built-in
fetchand the native test runner before adding dependencies for simple needs. - Never block the main thread with synchronous file or network calls in request handlers.