skip to content

When should a handler stop returning a plain object and return an explicit response wrapper or a redirect result instead?

level: seniorimportance: must knowfreq 52%

answer

  1. inference only says body plus success
  2. anything else varies, wrap it
  3. the branch is the value
  4. status and target travel together
  5. never redirect to untrusted input

basics

~20 s

Switch to a wrapper as soon as anything beyond the body varies: a status the default cannot reach, a header derived from the result, or several outcomes from one handler. A redirect result packages status and target together.

solid answer

~50 s

Plain-object returns are fine while the only thing that varies is the payload. The moment a response needs a **status the default cannot produce**, a **header derived from the result** — a location for a newly created resource, a validator token, a caching directive, a download filename — or the handler has **several outcomes on different branches**, the inference stops carrying enough information and you return a **wrapper** that holds status, headers and body together. A **redirect result** is the same mechanism specialised: instead of hand-writing a status and a target header and hoping the two stay consistent, you return one value that means "send the client elsewhere" and the framework emits both. The advantage over mutating the response is that the whole response stays a **value** — one return type per branch, assertable in a test without a live server.

go deeper

for a junior

Learn the trigger: if the endpoint needs a specific status or a header tied to the result, the handler must return a wrapper rather than the plain object.

for a middle

Explain what a wrapper carries and why a redirect result exists — a status and a target that must agree, packaged as one value so they cannot be written inconsistently.

for a senior

Show the production judgment: keep branches symmetric so errors never inherit the success default, keep cross-cutting headers in a shared stage, and validate every redirect target that came from a request.

for a principal

Decide where the line sits between inferred and explicit returns for the whole service, and make it enforceable, so one team's habit does not become another team's silent 200.

## What inference can and cannot state The default mapping states exactly one thing: *here is a body, and it succeeded*. That is a large fraction of endpoints and there is nothing wrong with leaning on it. It cannot state any of the following: - a status other than the default success; - any header whose value depends on **this particular result**; - two different responses from two branches of the same handler; - an intentionally empty response with a chosen status; - a redirect, which is a status and a target that must agree. When a handler needs one of those, it has to say so, and the return position is the natural place. ## The wrapper: status, headers and body as one value A response wrapper is an ordinary value carrying the three parts of a response. The handler builds it and returns it; the framework reads the status and headers off it and serializes the body it holds exactly as it would a bare return. Why that is preferable to reaching for the response object and mutating it: 1. **The branch is the value.** Every path out of the handler produces one wrapper, so "what does this endpoint answer" is answered by reading the returns, not by tracing which lines ran. 2. **It is testable without transport.** A test calls the handler and asserts on status, headers and body as data. No server, no socket. 3. **The header cannot drift from the body.** A location header written by a side effect can outlive the branch that produced it; one carried inside the wrapper cannot. 4. **It composes.** Shared helpers can build a wrapper and hand it back up; a mutation on a response cannot be returned. The cost is verbosity. A handler that wraps a value only to say "succeeded, here is the body" adds noise without adding information, which is why wrapping *everything* is a convention choice rather than an obvious win. ## Redirect results A redirect is a status plus a target location, and the two are meaningless apart. Writing them separately is a classic source of half-formed responses — a redirect status with no target, or a target header on a status that ignores it. Frameworks therefore offer a **redirect result type**: one value naming the target, which maps to the paired status and header. Things that stay your responsibility even with a result type: - **The target must not come from untrusted input unchecked.** A redirect target taken from a query parameter and returned unchanged is an open-redirect hazard: the attacker uses your trusted origin as a springboard. Validate against an allowlist, or accept only paths relative to your own origin. - **Permanence is a contract, not a detail.** A permanently-marked redirect is cached and may be remembered by clients long after you change your mind; a temporary one is not. Choose deliberately. - **The method a client uses afterwards varies by status.** Some redirect statuses historically caused clients to switch to a safe method, others preserve the original. If a redirect follows a write, that difference is behavioural. ## Choosing per handler | Situation | Return | |---|---| | Success with a payload, nothing else varies | the plain object | | A header derived from this result | a wrapper carrying that header | | Branches that answer differently | a wrapper on every branch, so they are symmetric | | Deliberate empty response with a chosen status | a wrapper with no body | | Send the client elsewhere | a redirect result | | A header that applies no matter which branch ran | set it in a shared stage rather than repeating it in each wrapper | That last row matters: cross-cutting headers — correlation ids, security headers, general cache policy — belong in a shared pipeline stage, not copied into every wrapper, or they will be missing from exactly the branch nobody updated. ## The anti-patterns to name - **Asymmetric branches.** The happy path returns a wrapper and the error path returns a bare object, so the error inherits a success status. This is the single most common way an error ships as `200`. - **Wrapping a wrapper.** A wrapper containing an already-mapped value, producing a body nested one level deeper than the contract says. - **Mutating the response *and* returning a wrapper.** Two sources of truth for the same header, resolved differently by different frameworks. - **A redirect target straight from the request.** Open redirect, every time. ## What a good answer sounds like Give the trigger — anything beyond the body varies — then the three concrete cases (status, result-derived header, multiple branches), then why a returned value beats a mutated response for testing and consistency. Finish on redirects as a paired status-and-target that a result type keeps consistent, and flag the untrusted-target hazard unprompted. That last point is what interviewers are listening for at senior level.

  • Why does returning a wrapper beat setting the status and headers on the response object?
    Because the whole response stays a value. Each branch returns one object that a test can assert on without a server, helpers can build and return it, and a header cannot drift away from the branch that produced it. Mutation spreads the answer across the statements that happened to run.
  • A handler wraps its success path but returns a bare error object on failure. What goes wrong?
    The error inherits the default success mapping, so a failure ships as a success status with an error-shaped body. Clients that branch on status treat it as fine and alerting never fires. Keep branches symmetric: if one path returns a wrapper, every path does.
  • What must you check before returning a redirect whose target came from the request?
    That the target is one you are willing to send users to. Accept only paths relative to your own origin, or match against an allowlist of permitted destinations, and reject anything else rather than falling back to the raw value. Unvalidated targets turn your origin into a launchpad for phishing.

saying these in an interview costs you the question

  • Wraps the success path but returns a bare object on errors
  • Thinks a wrapper is needed even when only the body varies
  • Writes a redirect status and target location separately
  • Returns a request-supplied redirect target without validation
  • Copies cross-cutting headers into every wrapper instead of a shared stage
  • Sets headers on the response and returns a wrapper carrying the same ones