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, bodyres= reply toolkit:.json(),.status(),.redirect(),.send()reqis read-only by convention;resis write-only by design- Body parsing:
express.json()must come before routes, orreq.bodyisundefined - Send a response once per request, always
returnon early exits
Quick example
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/:id→req.params.id)req.query- query string as an object (?page=2&limit=10→req.query.page)req.body- parsed request payload, available afterexpress.json()orexpress.urlencoded()req.headers/req.get('Name')- incoming headersreq.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 withContent-Type: application/jsonres.send(text)- sends text or HTML, auto-detects content typeres.status(code)- sets HTTP status, returnsresfor chainingres.redirect('/path')- sends a 302 redirect; useres.redirect(301, '/path')for permanentres.sendFile('/abs/path')- streams a file to the clientres.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:
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
// 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
// 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
// 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 withres.status(200).end()immediately to avoid webhook timeout - Passport.js: read
req.user(attached by the auth middleware), thenres.redirect('/dashboard') - File uploads with Multer: access
req.file.pathafter the upload middleware runs - Session cookies: read
req.cookies.sessionIdwith cookie-parser, set a new one viares.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
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
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 readyA concise answer to help you respond confidently on this topic during an interview.