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:
| Property | What it holds | Requires |
|---|---|---|
req.params | Named route segments (/users/:id) | A route with :params |
req.query | Parsed query string after ? | Built in |
req.body | Parsed request payload | A body parser |
req.headers | All request headers (lowercased keys) | Built in |
req.method | HTTP verb (GET, POST, …) | Built in |
req.path | Path portion of the URL | Built in |
req.ip | Client IP address | Built in (trust proxy for real IP) |
req.cookies | Parsed cookies | cookie-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-Typematches. A JSON payload sent without theapplication/jsonheader leavesreq.bodyempty ({}), 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
reqinput: validate and coercereq.params,req.query, andreq.bodybefore using them. - Mount
express.json()andexpress.urlencoded()early soreq.bodyis populated for every route that needs it. - Match the request’s
Content-Typeto the parser you expect — mismatches silently leavereq.bodyempty. - Use
req.get(name)rather than indexingreq.headersto stay case-insensitive and explicit. - Set
trust proxywhen running behind a load balancer soreq.ipreflects the real client. - Prefer
req.pathfor routing logic andreq.originalUrlfor logging the full, unmodified URL.