Course outline

Express.js / Basic: request and response ownership / Lesson 4

Responses, status, headers and representation

Build one coherent HTTP response and distinguish sending, status selection and completed client delivery.

About 4 min readContent reviewed
On this page

Lesson goals

  • Choose response status, media type and public body deliberately.
  • Explain headersSent, HEAD and cache validators without assuming a response can be rewritten after streaming.

An HTTP response carries several related promises

A status code, headers and body form one response contract. res.status chooses a status; it does not by itself finish the response. res.json serializes a JSON representation and sends it, while res.send handles other supported body forms. Choose a stable public error shape instead of exposing a raw exception object. A 201 response can describe creation under the domain contract, and a 204 response has no content body. These transport conventions should reflect actual application outcomes rather than being selected merely because the handler reached its final line.

Project one authorized representation before sending

The fragment sets the status and cache policy before sending a private synthetic card. A private cache directive is a deliberate example for principal-specific data, not a substitute for access control. Determine whether the particular response may be stored at all, how long it may be reused and whether shared intermediaries are eligible. Avoid interpolating untrusted header values or constructing redirects from arbitrary user input. The snippet demonstrates sequencing only and uses an already-authorized card; it neither authenticates a caller nor measures what a browser or proxy actually caches.

javascript Illustrative private representation
app.get("/account/course-card", async (req,res) => {
  const card = await catalog.cardForPrincipal(req.principal);
  res.status(200);
  res.set("Cache-Control","private, no-store");
  return res.json({id:card.id,title:card.title});
});

The response is JSON with the declared cache policy. The application still owns principal construction and catalog authorization.

Headers cannot be redesigned after they are sent

Once headers or body bytes are committed, a later failure cannot generally become a clean new JSON error response with another status. res.headersSent indicates that headers have been sent, not that the client received and processed the whole body. Error middleware should delegate appropriately when that boundary has already been crossed, and streaming code needs its own cleanup and failure policy. Returning from the handler prevents later local statements but does not cancel an already running source automatically. Track completion, disconnect and cancellation as separate lifecycle facts.

Test bodyless and content-bearing result plans

The local function represents an application decision before any HTTP bytes exist. A delete result that removed the eligible item plans a 204 without a body; an already absent item plans the documented 404 JSON error. Either domain design could choose another repeat-delete policy, so the model makes its own choice explicit. This is useful for testing result mapping separately from a transport adapter. The assertions do not prove Node’s body suppression, header serialization or actual Express response behavior, which require different integration evidence.

javascript Complete offline response-plan model
function deletePlan(removed) {
  return removed ? {status:204} : {status:404,body:{code:"COURSE_NOT_FOUND"}};
}
const success = deletePlan(true);
const absent = deletePlan(false);
console.assert(success.status === 204 && !Object.hasOwn(success,"body"));
console.assert(absent.status === 404 && absent.body.code === "COURSE_NOT_FOUND");

Both assertions pass under the declared repeat-delete policy. The function returns objects only and sends no HTTP response.

Review HEAD and conditional caching with the representation

HEAD describes the headers of a corresponding GET without a response content body. Express can use a GET handler for HEAD when no earlier HEAD route handles it, so expensive domain reads may still occur unless the application designs otherwise. Conditional requests and validators can reduce transfer but require representation-aware freshness and identity. An ETag does not authorize access, and a shared cache must not reuse one principal’s private representation for another. Review status, Vary, cache policy and validators together; never treat a good hit rate as proof that the audience and freshness contract are correct.

Common errors and fixes

The handler calls status but never sends

Why it happens
Status selection was mistaken for response completion.
How to fix it
Finish through an appropriate send, json or end path.
Prevent it next time
Review every branch for a final completion owner.

A late error attempts another JSON response

Why it happens
The code ignored the committed-header boundary.
How to fix it
Check headersSent and use the documented error delegation path.
Prevent it next time
Test failure after the first streamed bytes separately.

A private card is reused for another user

Why it happens
Cache audience or keying did not match representation scope.
How to fix it
Correct private/no-store policy and principal-aware cache lifecycle.
Prevent it next time
Test sequential principals through the actual cache layer.

Check your understanding

1. Does headersSent prove that the client successfully consumed the complete JSON body?

Show worked solution

No. It identifies a server-side commitment boundary for headers. The connection can still fail before complete delivery, and the client can fail while processing the representation. Use appropriate completion and disconnect observations for the transport lifecycle, and do not turn that flag into evidence that a business action was acknowledged by the user.

2. Why can a HEAD request still trigger a database lookup in a GET handler?

Show worked solution

Express can dispatch HEAD through the corresponding GET handler when an earlier HEAD route does not handle it. Suppressing a response body does not automatically suppress the application code that builds it. Review expensive metadata paths and authorization under HEAD explicitly, and verify the selected implementation rather than inferring cost from the lack of transferred content.

Key takeaways

  • Status, headers and body form one committed response contract.
  • Caching and HEAD change transfer behavior without granting permission or eliminating source work.

Official references