📬 Request and Response Objects
Every Express route handler is handed the same two objects: req, everything the client sent you, and res, the toolkit for replying. Master these two and you can read any incoming request and craft exactly the response you intend — the daily bread of backend work.
Week 7 · Day 2 (Tuesday: Express.js Basics) · Lecture 3
🎯 Learning Objectives
By the end of this lesson, you will be able to:
- Read data from every part of a request:
params,query,body, andheaders - Choose the right response method —
res.send,res.json,res.sendFile,res.redirect - Set accurate HTTP status codes and chain them with response methods
- Set response headers and cookies, including secure cookie options
- Return a consistent, well-structured JSON response shape from an API
- Handle a POST request end-to-end: parse the body, validate it, and respond appropriately
Estimated Time: 70 minutes
Practice: Build a small user API that reads request data and returns a consistent response envelope.
In This Lesson
The Two Objects
When a request matches a route, Express calls your handler with (req, res). The request object (req) carries everything the client sent — the URL, headers, query string, and body. The response object (res) is how you reply — you set a status, maybe some headers, and send a body.
📮 The mail analogy
Think of a request/response cycle as a letter and its reply. The request is the envelope that arrived: its address is the URL and params, the postmarks and stamps are the headers, and the letter inside is the body. Your handler is the clerk who reads it. The response is the reply you post back: a delivery status (the status code), handling instructions (headers), and the letter itself (the body).
Reading the Request
Here are the request properties you'll reach for constantly, each shown with the URL or input that populates it:
| Property | What it holds | Example |
|---|---|---|
req.params | Route parameters | /users/:id → req.params.id |
req.query | Query-string values | ?q=express → req.query.q |
req.body | Parsed request body | req.body.email (needs a parser) |
req.headers | All HTTP headers | req.headers['user-agent'] |
req.method | The HTTP verb | 'GET', 'POST' |
req.path | Path portion of the URL | /users/profile |
req.originalUrl | Full URL as received | /users/12?sort=asc |
req.ip | Client IP address | '203.0.113.5' |
A single handler often reads several of these at once:
app.get('/users/:id/posts', (req, res) => {
const userId = req.params.id; // from the path
const page = Number(req.query.page) || 1; // from the ?query
const agent = req.get('User-Agent'); // a header, via req.get()
console.log(`User ${userId}, page ${page}, from ${agent}`);
res.json({ userId, page });
});
💡 req.get(name) reads a header, case-insensitively
req.get('Content-Type') and req.get('content-type') return the same thing. It's cleaner than digging into req.headers directly, and it handles header-name casing for you.
Working with the Body
The body is where POST, PUT, and PATCH requests carry their payload. Unlike params and query, the body is not parsed automatically — you must register a body-parsing middleware first, or req.body will be undefined.
const express = require('express');
const app = express();
// Built-in parsers — register these BEFORE your routes:
app.use(express.json()); // application/json
app.use(express.urlencoded({ extended: true })); // HTML form posts
app.post('/users', (req, res) => {
const { name, email, password } = req.body;
// Never trust the client — validate before using:
if (!name || !email || !password) {
return res.status(400).json({ error: 'name, email and password are required' });
}
// (Imagine we save the user here...)
res.status(201).json({ message: 'User created', user: { name, email } });
});
Send a matching request with curl:
curl -X POST http://localhost:3000/users \
-H "Content-Type: application/json" \
-d '{"name":"Ada","email":"ada@example.com","password":"secret"}'
Output
{"message":"User created","user":{"name":"Ada","email":"ada@example.com"}}
⚠️ req.body is undefined? Check three things
- Did you register
express.json()before this route? - Did the client send a
Content-Type: application/jsonheader? - Is the body actually valid JSON? Malformed JSON triggers a parse error, not a filled body.
Sending a Response
The response object offers a method for every kind of reply. You call exactly one terminating method per request — sending two responses throws an error.
| Method | Sends | Example |
|---|---|---|
res.send() | String, HTML, Buffer, or object | res.send('Hello'); |
res.json() | JSON (sets the header) | res.json({ ok: true }); |
res.status() | Sets status (chainable) | res.status(201).json(x); |
res.sendFile() | A file's contents | res.sendFile(absPath); |
res.download() | A file as an attachment | res.download(absPath); |
res.redirect() | A redirect to another URL | res.redirect('/login'); |
res.end() | Ends with no body | res.status(204).end(); |
send vs. json
// res.send() is flexible — it guesses the Content-Type:
app.get('/text', (req, res) => res.send('Hello World')); // text/html
app.get('/html', (req, res) => res.send('<h1>Hi</h1>')); // text/html
// res.json() is explicit — always application/json, always stringified:
app.get('/api/user', (req, res) => {
res.json({ id: 1, name: 'Ada', roles: ['admin'] });
});
For APIs, prefer res.json(): it makes your intent obvious and guarantees the correct content type even for arrays and edge cases like null.
Redirects and files
const path = require('path');
// Redirect (302 by default; pass a code for others):
app.get('/old', (req, res) => res.redirect('/new'));
app.get('/moved', (req, res) => res.redirect(301, '/new-home'));
// Send a file — use an absolute path:
app.get('/report', (req, res) => {
res.sendFile(path.join(__dirname, 'files', 'report.pdf'));
});
// Prompt a download in the browser:
app.get('/report/download', (req, res) => {
res.download(path.join(__dirname, 'files', 'report.pdf'), 'annual-report.pdf');
});
Status Codes
The status code is how a client (or another program) knows what happened without reading your message. Setting the right one is a hallmark of a well-behaved API. res.status() is chainable, so it reads naturally before the body method.
app.get('/api/products', (req, res) => {
res.json(products); // 200 OK is the default
});
app.post('/api/products', (req, res) => {
const created = create(req.body);
res.status(201).json(created); // 201 Created
});
app.get('/api/products/:id', (req, res) => {
const product = find(req.params.id);
if (!product) {
return res.status(404).json({ error: 'Product not found' }); // 404
}
res.json(product);
});
app.delete('/api/products/:id', (req, res) => {
remove(req.params.id);
res.status(204).end(); // 204 No Content — no body
});
💡 The status codes you'll use most
| Code | Meaning | When |
|---|---|---|
200 | OK | Successful GET/PUT with a body |
201 | Created | A POST created a new resource |
204 | No Content | Success with nothing to return (DELETE) |
400 | Bad Request | Client sent invalid data |
401 | Unauthorized | Authentication missing or failed |
403 | Forbidden | Authenticated but not allowed |
404 | Not Found | Resource doesn't exist |
500 | Server Error | Something broke on your side |
A Consistent Response Shape
APIs are far easier to consume when every response follows the same structure. A small helper keeps success and error replies uniform across your whole app:
// A single helper for every API response
function apiResponse(res, { data = null, error = null, status = 200 }) {
return res.status(status).json({
success: error === null,
timestamp: new Date().toISOString(),
data,
error
});
}
// Used with modern async handlers:
app.get('/api/users/:id', async (req, res, next) => {
try {
const user = await db.findUser(req.params.id);
if (!user) {
return apiResponse(res, { error: 'User not found', status: 404 });
}
return apiResponse(res, { data: user });
} catch (err) {
next(err); // let the error-handling middleware format 500s
}
});
Output — success and error share one shape
// 200 OK
{ "success": true, "timestamp": "2026-07-31T10:00:00.000Z", "data": { "id": 1 }, "error": null }
// 404 Not Found
{ "success": false, "timestamp": "2026-07-31T10:00:01.000Z", "data": null, "error": "User not found" }
Practice & Quiz
🏋️ Exercise 1: Echo the request
Goal: Write GET /inspect/:id that responds with a JSON object containing the route param, the page query value (defaulting to 1), and the request's User-Agent header.
💡 Hint
Read the param from req.params.id, the query from req.query.page (convert with Number), and the header with req.get('User-Agent').
✅ Solution
app.get('/inspect/:id', (req, res) => {
res.json({
id: req.params.id,
page: Number(req.query.page) || 1,
userAgent: req.get('User-Agent')
});
});
🏋️ Exercise 2: Validated POST with correct status codes
Goal: Write POST /api/items that requires a JSON name. Return 400 if it's missing, otherwise 201 with the created item. Use a consistent response shape.
✅ Solution
app.use(express.json());
let items = [];
app.post('/api/items', (req, res) => {
const { name } = req.body;
if (!name) {
return res.status(400).json({ success: false, error: 'name is required' });
}
const item = { id: items.length + 1, name };
items.push(item);
res.status(201).json({ success: true, data: item });
});
🎯 Quick Quiz
Question 1: Which status code best fits a successful POST that created a new resource?
Question 2: What makes req.body available in a handler?
Question 3: Which cookie flag stops client-side JavaScript from reading the cookie?
Best Practices & Pitfalls
✅ Do
- Validate and type-convert everything from
req.params,req.query, andreq.body - Return the most specific status code that fits the outcome
- Use
res.json()for API data and keep a single consistent response shape - Set
httpOnly,secure, andsameSiteon any session cookie returnyour response calls inside branches so code below doesn't run
❌ Don't
- Send more than one response per request — it throws "headers already sent"
- Read
req.bodywithout a body parser registered first - Return
200for everything — status codes are part of your API's contract - Put secrets or personal data in the URL/query string — they get logged
⚠️ The double-response bug
// 🐛 Both branches can run — the second response throws:
app.get('/x', (req, res) => {
if (!valid) res.status(400).json({ error: 'bad' }); // no return!
res.json({ ok: true }); // runs anyway → crash
});
// ✅ return early so only one response is sent:
app.get('/x', (req, res) => {
if (!valid) return res.status(400).json({ error: 'bad' });
res.json({ ok: true });
});
Summary
🎉 Key Takeaways
- The request exposes client data via
req.params,req.query,req.body, andreq.headers req.bodyonly exists after a body parser likeexpress.json()runs- The response replies with
res.json(),res.send(),res.sendFile(), orres.redirect()— one terminating call per request - Status codes are part of the contract: 2xx success, 4xx client error, 5xx server error
- Harden session cookies with
httpOnly,secure, andsameSite, and keep a consistent response shape
📚 Additional Resources
- Express — Request object API
- Express — Response object API
- MDN — HTTP response status codes
- MDN — Using HTTP cookies
🚀 What's Next?
You can now read any request and shape any response. The next step is turning these building blocks into a clean, predictable API design that other developers can pick up instantly — the conventions of REST principles.
🎉 req and res are second nature now!
Every endpoint you'll ever write comes down to reading req and shaping res — and you've got both.