Skip to content
Express.js ex requests 4 min read

The Request Object

Every Express route handler and piece of middleware receives a request object — conventionally named req — as its first argument. It is an enhanced version of Node’s native http.IncomingMessage, wrapping the raw HTTP request in a friendly API for reading the URL, headers, body, and connection details. Understanding what lives on req (and what has to be enabled before it appears) is the foundation of handling input safely in Express.

Anatomy of req

Express decorates the incoming request with a handful of properties that cover the parts of an HTTP message you actually need. Some are available out of the box; others — like req.body and req.cookies — only get populated once you mount the right middleware. Here is the map:

PropertyWhat it holdsRequires
req.paramsNamed route segments (/users/:id)A route with :params
req.queryParsed query string after ?Built in
req.bodyParsed request payloadA body parser
req.headersAll request headers (lowercased keys)Built in
req.methodHTTP verb (GET, POST, …)Built in
req.pathPath portion of the URLBuilt in
req.ipClient IP addressBuilt in (trust proxy for real IP)
req.cookiesParsed cookiescookie-parser middleware

Route data: req.params and req.query

req.params exposes the dynamic segments captured by a route’s :param placeholders, while req.query holds everything after the ? in the URL. Both are always populated for matching routes, and every value is a string until you convert it.

const express = require("express");
const app = express();

// GET /products/42?currency=usd&detailed=true
app.get("/products/:id", (req, res) => {
  res.json({
    id: req.params.id,
    currency: req.query.currency,
    detailed: req.query.detailed,
  });
});

app.listen(3000);

Output:

{ "id": "42", "currency": "usd", "detailed": "true" }

Note that detailed is the string "true", not a boolean — the query parser never coerces types. Validate and convert these values before trusting them.

Reading the body: req.body

The request body (JSON, form data, etc.) is not parsed by default. Accessing req.body without a parser mounted yields undefined. Modern Express ships JSON and URL-encoded parsers built in, so enable them with app.use:

app.use(express.json()); // application/json
app.use(express.urlencoded({ extended: true })); // form posts

app.post("/users", async (req, res) => {
  const { name, email } = req.body;
  if (!name || !email) {
    return res.status(400).json({ error: "name and email are required" });
  }
  const user = await db.createUser({ name, email });
  res.status(201).json(user);
});

A POST /users with header Content-Type: application/json and the payload {"name":"Ada","email":"[email protected]"} makes req.body the parsed object { name: "Ada", email: "[email protected]" }.

Gotcha: The parser only runs when the request’s Content-Type matches. A JSON payload sent without the application/json header leaves req.body empty ({}), which is a frequent source of “why is my body undefined?” bugs.

Headers, method, and path

req.headers is a plain object whose keys are lowercased regardless of how the client sent them, so req.headers["content-type"] works no matter the original casing. For convenience, req.get(name) (case-insensitive) is the idiomatic accessor. req.method and req.path describe the verb and the path portion of the URL respectively.

app.use((req, res, next) => {
  console.log(`${req.method} ${req.path} from ${req.ip}`);
  console.log("Accept:", req.get("Accept"));
  next();
});

Output:

GET /products/42 from ::1
Accept: application/json

req.path differs from req.originalUrl and req.url: req.path excludes the query string, while req.originalUrl preserves the full untouched URL even after routers rewrite req.url.

Client identity: req.ip and cookies

req.ip returns the remote address of the connection. Behind a reverse proxy or load balancer that address is the proxy’s, not the real client’s — enable proxy trust so Express reads the X-Forwarded-For header instead:

app.set("trust proxy", true);

Cookies require the cookie-parser package; once mounted, req.cookies holds an object of name/value pairs.

npm install cookie-parser
const cookieParser = require("cookie-parser");
app.use(cookieParser());

app.get("/profile", (req, res) => {
  const session = req.cookies.sid; // undefined if not set
  res.json({ session });
});

Express 5 notes

Express 5 keeps the req surface familiar but tightens a few edges. Properties such as req.query are now read-only getters (you can no longer reassign them), and the deprecated req.param(name) helper was removed — read from req.params, req.query, or req.body explicitly. The data-bearing properties (params, query, body, headers, cookies) behave the same across 4.x and 5.x.

Best Practices

  • Never trust raw req input: validate and coerce req.params, req.query, and req.body before using them.
  • Mount express.json() and express.urlencoded() early so req.body is populated for every route that needs it.
  • Match the request’s Content-Type to the parser you expect — mismatches silently leave req.body empty.
  • Use req.get(name) rather than indexing req.headers to stay case-insensitive and explicit.
  • Set trust proxy when running behind a load balancer so req.ip reflects the real client.
  • Prefer req.path for routing logic and req.originalUrl for logging the full, unmodified URL.
Last updated June 14, 2026
Was this helpful?