Skip to main content

📬 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, and headers
  • 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).

graph LR A[Client] -->|request| B[Express handler] B -->|reads req| C[params / query / body / headers] B -->|writes res| D[status / headers / body] D -->|response| A

Reading the Request

Here are the request properties you'll reach for constantly, each shown with the URL or input that populates it:

PropertyWhat it holdsExample
req.paramsRoute parameters/users/:idreq.params.id
req.queryQuery-string values?q=expressreq.query.q
req.bodyParsed request bodyreq.body.email (needs a parser)
req.headersAll HTTP headersreq.headers['user-agent']
req.methodThe HTTP verb'GET', 'POST'
req.pathPath portion of the URL/users/profile
req.originalUrlFull URL as received/users/12?sort=asc
req.ipClient 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/json header?
  • 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.

MethodSendsExample
res.send()String, HTML, Buffer, or objectres.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 contentsres.sendFile(absPath);
res.download()A file as an attachmentres.download(absPath);
res.redirect()A redirect to another URLres.redirect('/login');
res.end()Ends with no bodyres.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

CodeMeaningWhen
200OKSuccessful GET/PUT with a body
201CreatedA POST created a new resource
204No ContentSuccess with nothing to return (DELETE)
400Bad RequestClient sent invalid data
401UnauthorizedAuthentication missing or failed
403ForbiddenAuthenticated but not allowed
404Not FoundResource doesn't exist
500Server ErrorSomething broke on your side
Status code families: 2xx success, 4xx client error, 5xx server error 2xx success 4xx client's fault 5xx server's fault
The first digit tells the whole story: 2xx worked, 4xx blame the request, 5xx blame the server.

Headers & Cookies

Setting response headers

app.get('/api/data', (req, res) => {
  // One at a time:
  res.set('X-API-Version', '1.0.0');

  // Or several at once:
  res.set({
    'Cache-Control': 'no-cache',
    'X-Powered-By': 'Bootcamp'
  });

  res.json({ message: 'Hello' });
});

Cookies

Use res.cookie() to set a cookie and res.clearCookie() to remove it. For anything touching a session, always set the security options:

// A hardened session cookie:
app.post('/login', (req, res) => {
  // ...authenticate the user...
  res.cookie('sessionId', 'abc123', {
    httpOnly: true,                 // JS in the browser can't read it (blocks XSS theft)
    secure: true,                   // only sent over HTTPS
    sameSite: 'strict',             // not sent on cross-site requests (blocks CSRF)
    maxAge: 24 * 60 * 60 * 1000     // 1 day, in milliseconds
  });
  res.redirect('/dashboard');
});

app.post('/logout', (req, res) => {
  res.clearCookie('sessionId');
  res.redirect('/login');
});

✅ The three cookie flags that matter for security

  • httpOnly — keeps client-side JavaScript from reading the cookie, mitigating XSS token theft
  • secure — only transmits the cookie over HTTPS
  • sameSite: 'strict' (or 'lax') — limits cross-site sending, mitigating CSRF

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, and req.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, and sameSite on any session cookie
  • return your 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.body without a body parser registered first
  • Return 200 for 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, and req.headers
  • req.body only exists after a body parser like express.json() runs
  • The response replies with res.json(), res.send(), res.sendFile(), or res.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, and sameSite, and keep a consistent response shape

📚 Additional Resources

🚀 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.