Building an HTTP Server
Routes, handlers, and the request lifecycle from socket to response.
40 minDifficulty 2/5node · apiAI-writtenWritten by a model on 16 August 2026 and not yet read by a person. Checked automatically: schema, the pedagogical rules the hand-written material is held to, and every diagram parsed for real.
Before this
Why this exists
A raw TCP socket delivers bytes; an HTTP server has to parse a method, a path, headers, and a body out of those bytes before any application logic can even begin, and then decide which of possibly hundreds of routes should handle this specific request. A router is the piece that does that dispatch — matching a method and path to exactly the function that should run.
The mental model
A route is a rule: given this HTTP method and this path pattern, run this handler function. The router checks incoming requests against every registered route, in order, and calls the handler for the first one that matches — the handler receives a request object (everything about what came in) and a response object (the tool for sending something back).
How it works
Method and path together define a route, not path alone
`GET /users/5` and `DELETE /users/5` are entirely different routes despite sharing the same path — the METHOD is part of what's matched, which is why a single URL can support fetching, updating, and deleting the same resource via different verbs rather than needing separate URLs for each action.
Path parameters extract dynamic segments from the URL
A route defined as `/users/:id` matches `/users/5`, `/users/42`, or `/users/anything`, and makes the matched segment available as `req.params.id` inside the handler — the router does the extraction, so the handler just reads a plain string value rather than parsing the URL itself.
Route order matters when patterns could both match
If `/users/:id` is registered BEFORE `/users/me`, a request to `/users/me` matches the first pattern (`:id` captures 'me' as a literal string) and never reaches the more specific route below it — routers match top to bottom and stop at the first match, so more specific routes generally need to be registered before more general, pattern-matching ones.
Every handler owns responding exactly once
Calling `res.send()` (or `.json()`, `.end()`) sends the response and completes the request-response cycle — calling it a second time for the same request throws an error, because HTTP responses are sent exactly once. A handler that conditionally sends a response in one branch but forgets in another leaves the client's connection hanging indefinitely, waiting for a response that never comes.
The mechanism
An incoming request's method and path are compared against each registered route in order. The first matching route's handler is invoked with a request object (containing the parsed method, path, params, query, headers, and body) and a response object. The handler does whatever work is needed and calls a method on the response object exactly once to send data back, completing the cycle for this request.
sequenceDiagram
participant C as Client
participant R as Router
participant H as Handler
C->>R: GET /users/5
R->>R: match method + path pattern
R->>H: call handler(req, res)
H->>H: req.params.id = "5"
H->>C: res.json({ ... })What people get wrong
- The path alone determines which route handles a request.
- The HTTP method is an equally important part of the match — `/users/5` under GET and under DELETE are entirely separate routes that can have completely different handlers. Forgetting method matters leads to registering conflicting or missing routes, like defining a DELETE handler but expecting a GET request to somehow also trigger it.
- Route registration order doesn't matter as long as all the routes exist somewhere.
- Routers match the FIRST route whose pattern fits, top to bottom — a more general pattern registered before a more specific one can silently swallow requests meant for the specific route, which never gets reached. This produces a specific, confusing bug class: a route exists in the code, is never actually broken, and yet never runs, because an earlier route with an overlapping pattern always matches first.
- It's fine to call res.send() (or similar) more than once if you're not sure which branch of your logic already handled it.
- Calling a response-sending method more than once for the same request throws an error ('headers already sent') — the fix is structuring the handler's control flow (with early returns, typically) so exactly one response-sending call executes per request. This is a very common bug in handlers with several conditional branches, where more than one branch's send call can accidentally execute for the same request.
When not to use it
- You need logic (authentication, logging, parsing) to run for MANY routes, not just one specific handler.
- Middleware (see `middleware-pattern`), which runs before a route's specific handler and can apply to a whole group of routes at once, rather than duplicating the same logic inside every individual handler.
- The API needs to support many resources with a consistent, predictable URL structure.
- A router grouped per resource (e.g. a dedicated router for all `/users/*` routes, mounted at that prefix), keeping related routes organized together rather than one flat, unstructured list.
Terms
- Route
- — A rule matching a specific HTTP method and path pattern to a handler function that processes matching requests.
- Path parameter
- — A dynamic segment of a route's path pattern (like :id) that's extracted from the actual URL and made available to the handler.
- Handler
- — The function that runs when a route matches an incoming request, receiving request and response objects and responsible for sending a response.
- Request lifecycle
- — The sequence from an incoming request being received, matched to a route, processed by middleware and a handler, and a response being sent back.
In an interview
A route defined as app.get('/users/:id', ...) is registered before app.get('/users/me', ...). What breaks, and why?
- a request to /users/me matches the :id pattern first, with 'me' captured as the id parameter
- the more specific /users/me route is registered after, so it's never reached — the router stops at the first match
- fix: register /users/me before /users/:id, since more specific routes need to come first
Can you recall it?
Why does registering `/users/:id` before `/users/me` cause requests to `/users/me` to never reach the intended `/users/me` handler?