Request & Response
Here's the whole job of a route handler, stripped of ceremony: it reads from req and writes
through res. Input comes in on the request object — the URL's params, the query string, the parsed
body, the headers. You do something with that input. Then you reach for the response object and send
exactly one answer back: a status code and usually some JSON.
That's the mental model for this entire phase. A handler is a small machine with one input port (req)
and one output port (res). Everything Express gives you here is just more knobs on those two ports.
📝 One thing that trips up everyone once:
req.bodydoes not exist by default. It only gets populated if a body-parsing middleware ran first —express.json()from Phase 3: Middleware. No parser, noreq.body. Hold that thought; we'll hit it again with code.
Reading the request
The req object carries four sources of input. You'll use all four constantly, so let's name them
plainly.
;
const app = ;
app.; // so req.body works for JSON requests
app.;
app.;
What just happened: The same request handed us input through four doors. req.params holds the
named pieces of the route pattern (:id became '42'). req.query holds everything after the ? in
the URL. req.body holds the parsed request body — but only because express.json() ran first.
req.headers is the raw header object, and req.get('Authorization') is the case-insensitive,
read-one-header convenience version. Notice req.params.id is the string '42', not the number 42 —
the URL is text, so everything from params and query arrives as strings. Convert when you need a
number.
⚠️ If you forget
app.use(express.json())and then readreq.body, you won't get an error — you'll getundefined. That silentundefinedis the single most common "why is my POST broken" moment in Express. Whenreq.bodyis empty and you swear you sent a body, check the parser first.
Writing the response
Now the output port. The res object is how you reply, and the methods you'll live in are small.
app.;
app.;
app.;
What just happened: res.json(obj) serializes an object to JSON and sets the Content-Type header
for you — it's the workhorse. res.status(code) sets the status code and returns res, which is
why you can chain it: res.status(201).json(task) reads almost like a sentence. res.sendStatus(204)
is the shorthand for "send this status with an empty body" — perfect for a successful delete where there's
nothing to return. A few more you'll meet: res.set('X-Foo', 'bar') sets a custom header, res.send(...)
sends text/HTML/buffers, and res.redirect(url) sends a redirect.
⚠️ Send exactly one response per request. Each
req/respair gets one reply, and once you've sent it the headers are flushed. Callres.json()(or any send) a second time and Express throwsError: Cannot set headers after they are sent to the client. This usually happens when you forget areturnafter an early response:res.; // 💥 headers already sent💡 The fix is a habit:
return res.status(404).json(...). Returning the response ends the handler right there.
Choosing honest status codes
The status code is a promise to the client about what happened. Lying with 200 OK on a failure makes
every consumer of your API guess. Use the codes that match reality:
- 200 OK — the standard "here's your data" success (a GET that found something).
- 201 Created — you created a resource (a successful POST). Often paired with the new object in the body.
- 204 No Content — success, but there's nothing to send back (a DELETE).
- 400 Bad Request — the client sent something wrong (missing or invalid input). This is their fault.
- 404 Not Found — the thing they asked for doesn't exist.
💡 Rough rule of thumb: 2xx means "it worked," 4xx means "you (the client) messed up," 5xx means
"I (the server) messed up." Reaching for the honest code costs you nothing and saves whoever calls your
API hours of confusion.
Never trust the input — validate it
Express has no built-in validation. None. It happily hands you whatever the client sent, including nothing, garbage, or hostile junk. That's not a gap to apologize for — it's the minimalist philosophy. But it means validation is your job, and skipping it is how APIs crash on a missing field or save nonsense to the database.
The simplest approach is a manual guard at the top of the handler: check what you require, and bail
early with 400 if it's missing.
app.;
What just happened: Before we trusted title for anything, we checked it. We pulled it out of
req.body (defaulting to {} so we don't crash if the body parser produced nothing), confirmed it's a
non-empty string, and if not, we returned a 400 with a message that tells the caller exactly what's
wrong. Only past that guard do we build the task — by then title is known-good. The early return is
doing double duty: it sends one response and it stops the handler, so we never fall through to the
201.
For one or two fields, a hand-written guard like this is honest and readable. As the rules grow (optional fields, types, lengths, nested objects), they get noisy, and that's when you reach for a library: express-validator layers validation onto the request, or a schema library like zod or joi lets you declare the shape once and validate against it. Same principle, less repetition.
⚠️ The rule never bends: never trust client input. Validate before you read it, save it, or pass it anywhere. Anyone can send any bytes to your endpoint — assume someone will.
Recap
- A handler reads from
req(params,query,body,headers/req.get()) and writes throughres(status + body). req.bodyonly exists if a body parser likeexpress.json()ran first — otherwise it'sundefined, silently.- Reply with
res.json(obj), set the code with the chainableres.status(code).json(obj), and useres.sendStatus(204)for empty successes. - Send exactly one response per request — a second send throws "Cannot set headers after they are sent." Habitually
returnyour responses. - Pick honest status codes: 201 created, 400 bad input, 404 not found, 204 no content.
- Express has no built-in validation. Guard required input by hand (return 400) or use express-validator/zod — and never trust the client.
Quick check
[
{
"q": "You POST JSON to an Express route and read req.body, but it's undefined. What's the most likely cause?",
"choices": ["The client didn't send a body", "You forgot app.use(express.json()) so no body parser ran", "req.body was renamed to req.data", "Express only parses bodies on GET requests"],
"answer": 1,
"explain": "req.body is only populated if a body-parsing middleware like express.json() ran first. Without it, req.body is silently undefined."
},
{
"q": "Which line sends a '201 Created' response with the new task as JSON?",
"choices": ["res.json(task).status(201)", "res.status(201).json(task)", "res.sendStatus(201, task)", "res.created(task)"],
"answer": 1,
"explain": "res.status(code) returns res so it's chainable: set the status first, then send the body with res.json(task)."
},
{
"q": "Your handler validates input and returns a 400 if a field is missing, but you still get 'Cannot set headers after they are sent.' What's the fix?",
"choices": ["Call res.json() twice on purpose", "Use return res.status(400).json(...) so the handler stops after responding", "Add a second express.json() middleware", "Switch the status code to 200"],
"answer": 1,
"explain": "Without return, code keeps running after the early response and sends a second one. Returning the response ends the handler so only one response is sent."
}
]
← Phase 3: Middleware · Guide overview · Phase 5: Building a REST API →
Check your understanding
1. You POST JSON to an Express route and read req.body, but it's undefined. What's the most likely cause?
2. Which line sends a '201 Created' response with the new task as JSON?
3. Your handler validates input and returns a 400 if a field is missing, but you still get 'Cannot set headers after they are sent.' What's the fix?