Skip to content

HTTP & Networking

One of Node.js’s defining strengths is its first-class networking support baked directly into the runtime. Before frameworks like Express or Fastify existed, developers wrote HTTP servers using nothing but the built-in node:http module — and that module is still the engine under every framework today.

This module teaches you to work with HTTP at the level that matters: the request/response cycle, routing, headers, status codes, and outbound fetching. By the end you will understand what every framework does under the hood.

LessonTopic
HTTP Serverhttp.createServer, status codes, headers, res.end
RoutingManual routing on req.method and req.url
Outbound RequestsGlobal fetch, JSON, error handling
JSON APIParse body, route, respond with JSON
Production ServerError handling, limits, timeouts, graceful shutdown

Every request follows the same shape, no matter which lesson below you are in: Node parses the incoming bytes into a req object, your callback runs, you write a res, and the connection either closes or waits for the next request:

flowchart TD
A["Client sends an HTTP request"] --> B["Node parses headers into an IncomingMessage (req)"]
B --> C["The request listener callback runs"]
C --> D["Handler reads the body, if any (data/end events)"]
D --> E["Handler calls res.writeHead(status, headers)"]
E --> F["Handler calls res.end(body)"]
F --> G{"Connection: keep-alive?"}
G -->|yes| H["Socket stays open, waits for the next request"]
G -->|no| I["Socket closes"]
One HTTP request, start to finish

Later lessons in this module fill in the sharp edges of this diagram: what happens if step D never finishes (body limits), what happens if step E or F is skipped or repeated (error listeners, ERR_HTTP_HEADERS_SENT), and how “waits for the next request” is bounded by real timeouts.

No npm install, no framework, no config files. This is a complete Node.js HTTP server:

Node.js

Needs the Node.js runtime — open in StackBlitz to run.

Three lines of logic — that is all it takes. The rest of this module unpacks each part.

Think of node:http as the actual wire-protocol implementation, and every framework — Express, Fastify, Hono, Koa — as ergonomics layered on top of the same req/res objects from the diagram above. A framework’s app.get('/path', handler) is a router that eventually calls res.writeHead() and res.end() for you; a middleware chain is just a sequence of functions sharing the same req/res. Nothing a framework does is magic — it is all reachable with the module you already have installed.

Writing directly against node:http gives you full control and zero dependencies, but it also means you own every edge case this module covers: body size limits, malformed input, timeouts, graceful shutdown. A framework buys productivity — routing, middleware, validation helpers — by making reasonable choices for those edge cases on your behalf. Neither is “correct”: a small internal tool might be better off with three routes and no framework at all, while a public-facing API benefits from a framework’s tested defaults. Either way, understanding the raw model means a framework’s behavior is never a black box.

  • “You need Express to build a real HTTP server.” node:http already is a real, production-capable HTTP server — Express adds routing conventions and middleware, not HTTP itself.
  • “http.createServer handles error cases for you.” It does not. Forgetting an 'error' listener, a body size limit, or a shutdown handler are all silent gaps you have to fill yourself — covered later in this module.
  • “A demo that works once is production ready.” A demo proves the happy path. The rest of this module is about the paths that are not happy: bad input, slow clients, a server that needs to restart.

AI-generated node:http servers reliably nail the happy path — a route that responds correctly — and just as reliably skip the response lifecycle details that only show up under real traffic, like actually finishing every response.

🐛 Spot the bug

const http = require('node:http');
const server = http.createServer(function (req, res) {
if (req.url === '/health') {
res.writeHead(200, { 'Content-Type': 'text/plain' });
// TODO: send the body once we decide on a format
} else {
res.writeHead(404, { 'Content-Type': 'text/plain' });
res.end('Not found');
}
});
server.listen(3000);
Show the answer

The /health branch calls res.writeHead() but never calls res.end(). Node does not close the response for you — the client is left waiting until it eventually times out on its own. The fix is the same one-liner every branch needs: make sure every code path that starts a response also calls res.end() (or res.destroy() if you are deliberately aborting it).

Which module do you use to create an HTTP server in Node.js without any npm packages?
What does the node: prefix in require("node:http") indicate?
Which frameworks use node:http under the hood?
What happens when a matched route never calls res.end()?
const server = http.createServer(function (req, res) {
  if (req.url === '/health') {
    res.writeHead(200, { 'Content-Type': 'text/plain' });
  }
});
In the minimal server demo, why does the code call server.close() inside the fetch(...).then(...) callback instead of right after server.listen(0, ...)?