Request & Response
The whole job of a route handler, stripped of ceremony: it reads from req and writes through
res. Input arrives on the request object - URL params, query string, parsed body, headers. You do
something with it, then reach for the response object and send exactly one answer back: a status code
and usually some JSON.
A handler is a small machine with one input port (req) and one output port (res). Everything in
this phase 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 = ;
; // so req.body works for JSON requests
;
;
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 ?.
req.body holds the parsed body - but only because express.json() ran first. req.headers is
the raw header object; req.get('Authorization') is the case-insensitive, one-header shortcut. Note
req.params.id is the string '42', not the number 42 - everything from params and query is
text. 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.
;
;
;
What just happened: res.json(obj) serializes an object to JSON and sets Content-Type for you -
the workhorse. res.status(code) sets the status code and returns res, so you can chain it:
res.status(201).json(task). res.sendStatus(204) sends a status with an empty body - perfect for a
successful delete. A few more you'll meet: res.set('X-Foo', 'bar') for a custom header, res.send(...)
for text/HTML/buffers, res.redirect(url) for 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:; // 💥 headers already sent💡 The fix is a habit:
return res.status(404).json(...). Returning the response ends the handler right there.
Choosing correct 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 accurate 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.
;
What just happened: before trusting title for anything, we checked it - pulled it from req.body
(defaulting to {} so a missing body doesn't crash us), confirmed it's a non-empty string, and
returned a 400 with a specific message if not. Only past that guard do we build the task. The early
return does double duty: it sends one response and stops the handler.
For one or two fields, a hand-written guard is clear and readable. As rules grow (optional fields, types, lengths, nested objects), 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.
⚠️ 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 correct 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."
}
]
Before the quiz: without looking back, say (or jot down) the core idea of this phase in your own words.
Check your understanding 3 questions
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?