Skip to content

Request and Response in Depth

The req (request) and res (response) objects are at the heart of every Express handler. You’ve used req.params, req.query, and res.json() — now let’s see the full picture.

req represents the incoming HTTP request.

The parsed request body — available after express.json() middleware:

app.use(express.json())
app.post('/posts', (req, res) => {
const { title, body, userId } = req.body
// req.body is the parsed JSON object
})

TypeScript note: req.body is typed as any. Define an interface and cast:

interface CreatePostBody {
title: string
body: string
}
app.post('/posts', (req, res) => {
const { title, body } = req.body as CreatePostBody
})

HTTP request headers as a lowercase key-value object:

const contentType = req.headers['content-type']
const authHeader = req.headers['authorization'] // 'Bearer eyJ...'
const userAgent = req.headers['user-agent']

Headers carry metadata: content type, authorization tokens, and cache directives.

The HTTP method as an uppercase string: 'GET', 'POST', 'DELETE', etc.

// For request to GET /posts/42?sort=newest
req.path // '/posts/42'
req.url // '/posts/42?sort=newest'

The client’s IP address — useful for rate limiting and logging.

res is how you send data back to the client.

Sends a JSON response with Content-Type: application/json:

res.json({ id: 1, title: 'Hello' })
// → 200 OK, Content-Type: application/json, body: {"id":1,"title":"Hello"}

Sets the HTTP status code. Chain with .json():

res.status(201).json({ id: newPost.id }) // 201 Created
res.status(400).json({ error: 'Invalid' }) // 400 Bad Request
res.status(404).json({ error: 'Not found' }) // 404 Not Found
res.status(401).json({ error: 'Unauthorized' }) // 401 Unauthorized

Sends a status code with the default status message as the body:

res.sendStatus(204) // 204 No Content (for successful DELETE)
res.sendStatus(401) // 401 Unauthorized

Sets a response header:

res.set('X-Custom-Header', 'value')
res.set('Cache-Control', 'no-store')

An object for passing data between middleware functions in a request’s lifecycle:

// In auth middleware
res.locals.user = decodedJwtPayload
// In route handler
const user = res.locals.user

Consistent response shapes make your API easier to use:

// Success with data
res.json({ data: post })
// Created
res.status(201).json({ data: newPost })
// Error
res.status(400).json({ error: 'Title is required' })
// No content (successful delete)
res.sendStatus(204)

The Bulletin API uses a simple pattern: top-level data for success, top-level error (string) for failures.

  1. Log req.headers in a route handler and make a request — inspect the headers your browser/curl sends.
  2. Build a route that reads Authorization from req.headers, validates it isn’t empty, and returns 401 if missing.
  3. Create a helper function sendError(res, status, message) that reduces repetition in error responses.
  • req.body — parsed JSON body (requires express.json() middleware)
  • req.headers — lowercase key-value object of request headers
  • req.params — dynamic URL segments; req.query — query string values
  • res.json(data) — sends JSON with correct Content-Type
  • res.status(code).json(data) — sets status code before sending
  • res.locals — passes data between middleware in the same request cycle