Skip to main content

What are the req and res objects in Express.js?

req and res are the two objects Express passes into every route handler. req holds all incoming HTTP data. res gives you the tools to send a reply.

Theory

TL;DR

  • req = incoming data: URL params, query string, headers, body
  • res = reply toolkit: .json(), .status(), .redirect(), .send()
  • req is read-only by convention; res is write-only by design
  • Body parsing: express.json() must come before routes, or req.body is undefined
  • Send a response once per request, always return on early exits

Quick example

js
const express = require('express'); const app = express(); app.use(express.json()); // populates req.body app.get('/user/:id', (req, res) => { const id = req.params.id; // "123" from URL segment const name = req.query.name; // "Alice" from ?name=Alice const token = req.get('Authorization'); // request header value res.status(200).json({ id, name }); // sends JSON response }); app.listen(3000); // GET /user/123?name=Alice → { "id": "123", "name": "Alice" }

req pulls three separate pieces of data from one URL. res sends one structured reply. That split is the whole model.

What req holds

req is built on Node's http.IncomingMessage, but Express adds parsed properties on top. The properties you'll use in most handlers:

  • req.params - URL segments matched by the route pattern (/user/:idreq.params.id)
  • req.query - query string as an object (?page=2&limit=10req.query.page)
  • req.body - parsed request payload, available after express.json() or express.urlencoded()
  • req.headers / req.get('Name') - incoming headers
  • req.method, req.path, req.protocol, req.ip - request metadata

Middleware can also attach custom properties. Passport.js sets req.user, Multer sets req.file, and you can assign your own inside any custom middleware function.

What res does

res is built on Node's http.ServerResponse, extended with Express helpers. Core methods:

  • res.json(data) - sends JSON with Content-Type: application/json
  • res.send(text) - sends text or HTML, auto-detects content type
  • res.status(code) - sets HTTP status, returns res for chaining
  • res.redirect('/path') - sends a 302 redirect; use res.redirect(301, '/path') for permanent
  • res.sendFile('/abs/path') - streams a file to the client
  • res.set('Header', 'value') - sets a response header

Most res methods return res itself, so chaining works: res.status(201).json({ ok: true }).

res.locals is a plain object that lives for one request cycle. Middleware writes data there, route handlers read it:

js
app.use((req, res, next) => { res.locals.requestId = crypto.randomUUID(); next(); }); app.get('/profile', (req, res) => { res.json({ requestId: res.locals.requestId }); });

When to use what

  • POST or PATCH data → req.body (after body-parser middleware)
  • Resource IDs in URL → req.params
  • Filters, sorting, pagination → req.query
  • Auth tokens → req.get('Authorization')
  • JSON API response → res.json()
  • Error responses → res.status(400).json({ error: '...' })
  • Redirects → res.redirect('/path')

Common mistakes

Accessing req.body without a body parser

js
// Wrong: req.body is undefined app.post('/login', (req, res) => { console.log(req.body); // undefined }); // Fix: register before routes app.use(express.json());

Missing return on early response calls

js
// Wrong: sends two responses, throws "Cannot set headers after they are sent" app.get('/user/:id', (req, res) => { if (!req.params.id) res.status(400).send('No ID'); res.json({ ok: true }); // always runs }); // Fix app.get('/user/:id', (req, res) => { if (!req.params.id) return res.status(400).send('No ID'); res.json({ ok: true }); });

This is the most common Express bug I see in code reviews. The first response goes out fine, but the handler keeps running and crashes on the second res call.

Calling res.json() after res.write()

Once you start streaming with res.write(), headers are already sent. Calling res.json() afterward throws an error. Finish streaming with res.end() only.

Logging full req.body in production

js
// Leaks passwords and PII to your logs console.log(req.body); // Log only what you need console.log({ email: req.body.email });

Real-world usage

  • Stripe webhooks: read event from req.body, reply with res.status(200).end() immediately to avoid webhook timeout
  • Passport.js: read req.user (attached by the auth middleware), then res.redirect('/dashboard')
  • File uploads with Multer: access req.file.path after the upload middleware runs
  • Session cookies: read req.cookies.sessionId with cookie-parser, set a new one via res.cookie('token', value, { httpOnly: true })

Follow-up questions

Q: What is the difference between req.params, req.query, and req.body?


A: req.params comes from named URL segments like /user/:id. req.query comes from the query string ?key=val. req.body comes from the POST or PUT request payload and needs a parser middleware to be available.

Q: What happens if you call res.send() twice?


A: Express throws ERR_HTTP_HEADERS_SENT. The first call closes the response stream. Always return after any res call that ends the handler.

Q: How does express.json() populate req.body internally?


A: It listens to the raw data and end events on req, collects chunks into a buffer, then calls JSON.parse() on the result. The default size limit is 100kb, which prevents memory exhaustion from large payloads.

Q: How do you get the real client IP when Express sits behind an Nginx proxy?


A: Set app.set('trust proxy', 1), then read req.ip. Without that setting, Express reads the proxy server's own address instead of the X-Forwarded-For header value.

Examples

Product lookup: params and query together

js
const express = require('express'); const app = express(); app.get('/products/:id', (req, res) => { const productId = req.params.id; const currency = req.query.currency || 'USD'; // In real code, fetch from DB here res.json({ productId, currency }); // GET /products/42?currency=EUR → { "productId": "42", "currency": "EUR" } }); app.listen(3000);

Clean GET pattern: req.params for the resource identifier, req.query for options, res.json() for the reply.

Login handler: body validation with early returns

js
app.use(express.json()); app.post('/login', (req, res) => { const { email, password } = req.body; if (!email || !password) { return res.status(400).json({ error: 'Missing credentials' }); } if (email === 'user@example.com' && password === 'secret') { return res.json({ token: 'jwt-token', user: { email } }); } res.status(401).json({ error: 'Invalid credentials' }); });

Every early exit uses return. Without it, Node tries to send a second response after the if-block exits, which crashes the request with the headers-already-sent error.

Short Answer

Interview ready
Premium

A concise answer to help you respond confidently on this topic during an interview.

Finished reading?