Last updated: 2026-09-23

Request & Response API

5.1 The Request Object: What You're Working With

Every middleware function and route handler receives a req object. It's the first argument, and it carries everything the client sent your way. The Express documentation describes it as "an enhanced version of Node's own request object" — meaning it has all the native Node properties you'd expect, plus Express-specific conveniences layered on top .

The request object is your window into what the client wants. It tells you the URL they hit, the method they used, the data they sent, the headers they included, and a lot more. Understanding which properties matter and when they're populated is the difference between writing clean handlers and writing handlers that mysteriously break.

5.2 The Request Properties You'll Actually Use

req.params

Route parameters live here. If you defined a route as /users/:userId, a request to /users/42 populates req.params.userId with the string '42' .

app.get('/users/:userId/posts/:postId', (req, res) => {
  const { userId, postId } = req.params;
  res.json({ userId, postId });
});

A request to /users/42/posts/99 gives you { userId: '42', postId: '99' }.

A subtle Express 5 change: wildcard parameters are now arrays of decoded path segments rather than single strings. A route like /*splat matching /a/b/c produces req.params.splat === ['a', 'b', 'c'], not 'a/b/c' .

Another nuance: parameters defined in optional segments that aren't present in the request are omitted from req.params entirely. And changes you make to req.params in one middleware function are reset before the next handler runs .

req.query

The parsed query string. For a request to /search?q=express&limit=10, you get { q: 'express', limit: '10' } .

The parsing behavior is configurable via the query parser application setting. The default uses the qs library, which supports nested objects and arrays, but you can switch to a simpler parser or disable it entirely .

Security caveat worth internalizing: req.query's shape is based on user-controlled input. Every property and value in it is untrusted. The Express documentation warns explicitly that req.query.foo.toString() may fail in multiple ways — foo might not exist, might not be a string, or toString might not even be a function . Validate before you trust.

req.body

The parsed request body. This is populated by body-parsing middleware — express.json() for JSON payloads, express.urlencoded() for form submissions. Without that middleware, req.body is undefined .

app.use(express.json());

app.post('/users', (req, res) => {
  const { name, email } = req.body;
  // ...
});

The same security warning applies here, and it's even more critical. req.body is built from user input. Every property is untrusted. The documentation gives a concrete example: req.body.foo.toString() may fail because foo isn't there, isn't a string, or toString is a string instead of a function . Never assume the shape of incoming data.

req.headers

The raw request headers. Express provides a cleaner way to access individual headers:

const contentType = req.get('Content-Type');
const auth = req.get('Authorization');

req.get() is case-insensitive and returns undefined if the header isn't present.

req.method

The HTTP method as a string: 'GET', 'POST', 'PUT', 'DELETE', etc. .

req.path and req.originalUrl

req.path contains the path portion of the URL, without the query string. When called inside mounted middleware, the mount point is stripped out .

req.originalUrl retains the full original URL — mount point included — and is available in both middleware and routers. It's a combination of req.baseUrl and req.url .

// Mounted at /api
app.use('/api', (req, res, next) => {
  console.log(req.path);        // '/users' (mount stripped)
  console.log(req.originalUrl); // '/api/users' (full URL)
  next();
});

req.hostname and req.ip

req.hostname gives you the hostname from the Host header, without the port. req.ip gives you the remote IP address .

Both are affected by the trust proxy setting. Behind a reverse proxy, the real client IP is in the X-Forwarded-For header, and Express only reads it when trust proxy is configured. Without that setting, req.ip returns the proxy's address, not the client's .

req.fresh

A boolean indicating whether the response is still fresh in the client's cache. When true, you can skip sending the full response — the client already has it .

app.get('/data', (req, res) => {
  if (req.fresh) {
    res.status(304).end(); // Not Modified
    return;
  }
  res.json(getData());
});

5.3 The Response Object: Sending Data Back

The res object is your tool for constructing and sending responses. Like req, it's an enhanced version of Node's native response object, with Express-specific methods that handle common patterns cleanly .

The response object is the second argument to every route handler and middleware function. Once you call a method that sends data — res.send(), res.json(), res.end() — the response is committed. You can't send it again.

5.4 The Core Response Methods

res.send(body)

The workhorse. Sends a response and automatically sets the Content-Type based on what you pass :

res.send('Hello World');           // text/html
res.send({ message: 'Hello' });    // application/json
res.send(Buffer.from('binary'));   // application/octet-stream

For objects and arrays, res.send() serializes to JSON. For strings, it sends HTML by default.

res.json(obj)

Explicit JSON. Sets Content-Type: application/json and serializes the object :

res.json({ users: [{ id: 1, name: 'Alice' }] });

In practice, res.send() with an object does the same thing, but res.json() is clearer about intent and handles edge cases like null more predictably.

res.status(code)

Sets the HTTP status code. Chainable :

res.status(404).send('Not found');
res.status(201).json({ created: true });
res.status(500).json({ error: 'Something broke' });

Common codes: 200 (OK), 201 (Created), 400 (Bad Request), 401 (Unauthorized), 404 (Not Found), 500 (Internal Server Error).

res.redirect([status], path)

Redirects the client. Defaults to 302 (Found) :

res.redirect('/new-page');              // 302
res.redirect(301, '/permanent-page');   // 301 (Permanent)
res.redirect('<https://example.com>');    // External URL
res.redirect('../up-one-level');        // Path-relative

The back redirect value was deprecated in Express 4.21.0. Use req.get('Referrer') || '/' instead .

res.render(view, locals)

Renders a template and sends the HTML :

app.get('/profile', (req, res) => {
  res.render('profile', { name: 'John', email: 'john@example.com' });
});

The template engine must be configured — app.set('view engine', 'pug') or similar — and the view file must exist in the configured views directory.

res.sendFile(path)

Sends a file from disk :

res.sendFile('/absolute/path/to/file.pdf');
res.sendFile('report.pdf', { root: '/files' });

The path must be absolute, or you must provide the root option. This method has security implications — if the path is constructed from user input, ensure it can't escape the intended directory .

res.set(field, [value]) and res.header()

Sets response headers :

res.set('Content-Type', 'text/plain');
res.set({ 'X-Custom': 'value', 'Cache-Control': 'no-store' });

res.type(type)

Shorthand for setting Content-Type :

res.type('html');        // text/html
res.type('application/json');
res.type('.pdf');        // application/pdf

5.5 Response Lifecycle and res.locals

res.locals is an object scoped to the current request. Anything you put there is available to templates rendered during that request :

app.use((req, res, next) => {
  res.locals.user = req.session?.user || null;
  res.locals.requestTime = new Date().toISOString();
  next();
});

app.get('/dashboard', (req, res) => {
  res.render('dashboard'); // Has access to res.locals.user
});

This is the clean way to pass request-specific data to views without threading it through every function call.

Once a response is sent — via res.send(), res.json(), res.end(), or any other terminal method — the lifecycle ends. Any headers set after that point are ignored, and calling response methods again may throw errors.

5.6 Extending the Request and Response Objects

Express allows you to override or extend its API. The documentation covers this under "Overriding the Express API" — you can assign custom functions to modify existing methods or add new ones .

A common pattern is adding custom properties to req in middleware:

app.use((req, res, next) => {
  req.user = authenticate(req); // Custom property
  next();
});

app.get('/profile', (req, res) => {
  res.json(req.user); // Available here
});

For TypeScript users, extending the Request interface requires module augmentation. The Express team notes that the request and response objects inherit from http.IncomingMessage.prototype and http.ServerResponse.prototype respectively, which is why the prototype chain matters when overriding methods .