Request/Response Lifecycle

The complete journey a request takes from the moment it arrives at your server to the moment a response is sent back.

What is it?

Every single interaction a backend has with the outside world follows the same overall shape: something asks for something (a request), and the server eventually answers (a response). But between those two moments, several distinct things happen in a predictable order. Understanding that full sequence — the request/response lifecycle — is what lets you reason about where to put logging, where auth checks belong, and why a response sometimes never arrives.

Explain like I'm 10

Ordering food through a drive-through: your car pulls up (a request arrives), you pass through the ordering speaker and the payment window (middleware), your order reaches the kitchen (the route handler), the food gets made (business logic runs), and finally it's handed to you through the pickup window (the response is sent). If any step along the way fails, you never reach the end.

Examples

Tracing one request through the whole lifecycle

app.use(express.json());          // 1. parse the request body
app.use(logger);                  // 2. log the request

app.get("/products/:id", (req, res) => {  // 3. matched route runs
  const product = findProduct(req.params.id); // 4. business logic
  if (!product) {
    return res.status(404).json({ error: "Not found" }); // 5a. response
  }
  res.json(product);             // 5b. response
});

Every request to this server flows through the same numbered stages, even though the specific data differs each time.

A request that ends early

app.use((req, res, next) => {
  if (isBlocked(req.ip)) {
    return res.status(403).send("Forbidden"); // lifecycle ends here
  }
  next();
});

app.get("/data", (req, res) => {
  res.json(getData()); // never reached for blocked IPs
});

The lifecycle doesn't always reach the route handler — any middleware along the way can send a response and end things early.

How it works

A request's journey generally looks like this: it arrives at the server; the framework parses low-level details like headers and the body; it flows through any globally-registered middleware in order; the framework matches it to a specific route based on method and path; any route-specific middleware runs; the route handler executes the actual application logic, often involving a database call; a response is constructed with a status code, headers, and a body; and that response is sent back over the network, closing out the request. Once a response has been sent, nothing more can be sent for that same request.

Client
  │  request
  ▼
[Parsing] → [Global middleware] → [Route matching]
                                        │
                                        ▼
                              [Route-specific middleware]
                                        │
                                        ▼
                                 [Route handler]
                                        │
                                        ▼
                                 [Build response]
                                        │
                                        ▼
Client ◄──────────────── response ─────┘

Why does it exist?

Thinking in terms of a fixed lifecycle gives every developer working on a backend a shared mental model of "where" any given piece of code runs relative to everything else — which is essential for debugging (why did this request never get logged?) and for deciding where new logic belongs.

When to use it

Keep this lifecycle in mind whenever you're deciding where a new piece of logic belongs — early as global middleware, scoped to one route, or inside the handler itself — or when debugging why a request behaved unexpectedly.

When not to use it

This isn't something you "use" directly — it's the backdrop every request already runs through. There's no scenario where it doesn't apply to an HTTP-based backend, though extremely different systems (like a raw TCP server with no framework) may structure it differently.

Common mistakes

  • Trying to send a second response after one has already been sent, causing a runtime error.

  • Not realizing that a middleware or route handler earlier in the chain already ended the lifecycle, so later code never runs.

  • Putting expensive, request-specific work in global middleware that runs on every request, even ones that don't need it.

Practice exercises

  1. Easy:

    List, in order, the stages a GET request passes through in a typical Express app with one logging middleware and one route.

  2. Medium:

    Write a small Express app where a middleware blocks requests without a specific header, and trace what happens to a request that is blocked versus one that isn't.

  3. Hard:

    Explain what error you'd expect if a route handler calls res.json() twice for the same request, and why.

Interview questions

What is the request/response lifecycle?

The full, ordered sequence of steps a request goes through from arriving at a server to a response being sent back — parsing, middleware, routing, the handler, and the final response.

Can the lifecycle end before reaching the route handler?

Yes — any middleware along the way can send a response itself and stop calling next(), ending the lifecycle early.

What happens if code tries to send a response after one has already been sent for the same request?

It typically causes a runtime error, since a single request can only be answered with one response.

List, in order, the main stages a request passes through in a typical Express app, from arrival to response.

Arrival at the server, parsing (headers and body), global middleware, route matching, route-specific middleware, the route handler running the actual application logic, the response being built (status, headers, body), and the response being sent back over the network.

Why does body-parsing like `express.json()` need to run before any middleware or handler that reads `req.body`?

req.body only exists as a usable object after something has read the raw incoming bytes and parsed them; anything registered earlier in the chain than the parser would see req.body as undefined or missing.

A global middleware checks `isBlocked(req.ip)` and returns a 403 without calling `next()`. What happens to that request's route handler?

It never runs — the lifecycle ends at that middleware since next() was never called, so the route handler downstream is completely skipped for blocked requests.

Why is understanding the lifecycle useful for debugging "why did this request never get logged"?

If logging middleware is registered after something that can short-circuit the request (like an early auth check or blocklist), a rejected request never reaches the logger; knowing the fixed order lets you reason about exactly which stages a given request actually passed through.

Where should logic that applies to every request go versus logic for just one route — early in the chain, or inside a specific handler?

Logic needed for every (or nearly every) request belongs in global middleware registered early, so it runs before routing decides which handler applies; logic specific to one endpoint belongs inside that route's own handler (or route-specific middleware), since applying it globally would waste work on requests that don't need it.

Why can a single request only ever receive one response, and what would break if that weren't true?

The underlying HTTP connection expects exactly one status line, one set of headers, and one body per request; sending a second response would mean writing conflicting data onto a connection the client is already treating as closed and answered, which is why frameworks throw an error rather than silently allowing it.

In the drive-through analogy, what corresponds to "the food gets made," and what corresponds to "the pickup window"?

"The food gets made" is the route handler running its business logic; "the pickup window" is the final response being constructed and sent back to the client, ending the lifecycle.

A route handler calls `res.json(product)`, and later in the same function unconditionally also calls `res.status(404).json({...})`. What bug results?

Since a response was already sent by the first call, the second call throws a "headers already sent" error — the fix is to return after the first response or make the second call conditional so only one of them ever executes for a given request.

Why does putting expensive, request-specific work in global middleware hurt performance even for requests that don't need it?

Global middleware runs on every request that passes through it, regardless of whether that particular request actually needs the work being done — so an expensive operation there adds latency to requests that would otherwise not have paid that cost at all.

With `app.use(express.json())`, then `app.use(logger)`, then a route, where does malformed JSON in the request body most likely cause a failure?

At the express.json() parsing stage, before logger or the route handler ever run — the body parser fails to parse the malformed JSON and typically triggers an error response, ending the lifecycle before the rest of the chain executes.

Give an example where a request not reaching the route handler is the intended, correct behavior.

A middleware rejecting an unauthenticated request with a 401 before it reaches a protected route — the lifecycle ending early here is exactly the desired outcome, not a bug.

What determines whether route-specific middleware runs before or after global middleware for a given request?

Global middleware is registered at the app level and always runs first for any matching request, since routing (which decides route-specific middleware) hasn't happened yet at that point; route-specific middleware only runs once the framework has matched the request to that particular route.

Why does the lifecycle model apply to essentially every HTTP-based backend, regardless of framework or language?

HTTP itself is fundamentally request-in, response-out, so any server built on it — no matter what language or framework — must receive a request, do some processing, and send back exactly one response; the specific stages in between (middleware, routing) are implementation details layered on top of that same basic shape.

To add authentication to just one route without affecting others, at which stage would you add that logic?

As route-specific middleware attached only to that route (or a group of routes), rather than as global middleware, so it runs as part of that route's own portion of the lifecycle without affecting requests to other routes.

From the client's point of view, can it tell whether a response came from an early short-circuiting middleware or the full lifecycle completing normally?

Not directly from the response alone — the client just receives a status code, headers, and body either way; only the specific content of the response (and knowledge of the API) reveals whether it represents a rejection partway through or the actual result of the route handler's logic.

Why is "building the response" treated as a distinct stage from the route handler having already computed a result?

Computing a result (like fetching a product from the database) and shaping it into an actual HTTP response (deciding the status code, setting headers, serializing the body) are different concerns — separating them conceptually makes clear that a handler's job isn't done just because it has data, it still has to decide how that data becomes a proper response.

Two middleware functions and the route handler all set the same header. Which value does the client actually receive, and why?

Whichever call runs last wins, since each call to set a header simply overwrites any previous value — because the route handler executes after all preceding middleware in the chain, its value typically takes effect unless a later middleware (registered after the handler, which is unusual) overwrites it again.