Request & Response
Express.js Fundamentals
Chapter 4 of 8 · Request & Response
Request & Response
Every Express route handler receives two enhanced objects: req (the incoming request) and res (the outgoing response). Express wraps Node's raw IncomingMessage and ServerResponse with a richer API, adding convenience methods for reading headers, parsing bodies, setting cookies, and sending structured responses. Knowing the full surface area of both objects means you stop re-implementing things Express already gives you for free.
The req Object
Identity Properties
These properties tell you what the client asked for.
URL Properties
req.method — 'GET', 'POST', …
req.path — path only, no query string ('/users/42')
req.url — path + query string ('/users/42?sort=asc'). Relative to the mount point of the current router.
req.originalUrl — full URL as received, even inside a nested router
req.hostname — host header without port ('api.example.com')
req.ip — client IP. Set app.set('trust proxy', 1) if behind a reverse proxy to read the real IP from X-Forwarded-For.
req.protocol — 'http' or 'https'
req.secure — shorthand for req.protocol === 'https'
Parsed Data
req.params — named URL segments (:id)
req.query — parsed query string object
req.body — parsed request body (needs express.json() or similar)
req.cookies — parsed cookies (needs cookie-parser)
req.signedCookies — cookies signed with a secret (needs cookie-parser with a secret)
req.headers — all request headers as a lowercase-keyed object
req.get() and req.is()
Two convenience methods for working with headers and content types:
Body Parsing in Depth
Without a body-parsing middleware, req.body is undefined. Express ships four built-in parsers; you pick the one that matches what clients send.
| Middleware | Parses | req.body type | When to use |
|---|---|---|---|
| express.json() | application/json |
Object / Array | REST APIs — the standard choice for JSON clients |
| express.urlencoded() | application/x-www-form-urlencoded |
Object | HTML form submissions (<form method="POST">) |
| express.raw() | application/octet-stream (or custom) |
Buffer | Webhook signature verification (Stripe, GitHub) — need raw bytes |
| express.text() | text/plain |
String | Plain text payloads, some webhook providers |
The res Object
Sending Responses
| Method | What it sends | Content-Type |
|---|---|---|
| res.json(data) | JSON-serialised object/array. Calls JSON.stringify. | application/json |
| res.send(body) | String → HTML. Buffer → binary. Object → JSON (calls res.json internally). | Auto-detected |
| res.sendStatus(code) | Status code + its name as plain text (404 Not Found). | text/plain |
| res.sendFile(path) | Streams a file. Path must be absolute. Sets Content-Type from extension. | Auto from ext |
| res.download(path, name?) | Like sendFile but adds Content-Disposition: attachment — triggers browser download. | Auto from ext |
| res.redirect(url) | 302 redirect by default. Pass a status code first: res.redirect(301, url). | — |
| res.end() | Ends the response with no body. Use res.sendStatus(204) instead for clarity. | — |
Response Headers
res.locals
res.locals is a plain object scoped to a single request. Middleware uses it to pass data to later middleware or route handlers without polluting req.
Cookies
Cookies are small pieces of data the server sends to the browser, which the browser then includes in all subsequent requests to that origin. Express reads cookies via req.cookies (requires cookie-parser middleware) and sets them via res.cookie().
Cookie Security Options
httpOnly: true — prevents JavaScript (including XSS payloads) from reading the cookie. Always set for session tokens.
secure: true — only sent over HTTPS. Set in production. In development, use process.env.NODE_ENV === 'production' to conditionally apply.
sameSite: 'strict' — cookie not sent on cross-site requests at all. 'lax' allows top-level navigations (e.g. clicking a link). Use 'strict' for auth cookies.
Cookie vs Authorization Header
Cookies — browser sends automatically; no JS required. Work well for browser-based apps. Require sameSite/httpOnly to be secure. CSRF protection required.
Authorization: Bearer <token> — must be set explicitly (JS reads token from storage). Suits SPAs and mobile clients. No CSRF risk since it must be set intentionally. No httpOnly equivalent.
Chapter 7 covers JWT auth in detail, including which storage approach to use.
⚠ Never Store Sensitive Data in Cookie Values
Cookie values are sent in plain text (only the transport is encrypted by HTTPS — the browser can still read them). Store an opaque session ID or a signed token, not the actual user data, permissions, or secrets. Signed cookies protect against tampering but not against reading — use httpOnly to prevent JS access, and HTTPS to prevent network interception.
💡 req.path vs req.url vs req.originalUrl
req.path — path only, no query string. Relative to the current router's mount point.
req.url — path + query string, relative to the router mount point. Express rewrites this internally as the request passes through nested routers.
req.originalUrl — the full URL as the client sent it, unchanged through all router levels. Use this in logging middleware — it's what the client actually requested.
Example: app mounts /api router, request is GET /api/users?sort=asc. Inside the router handler: req.path = '/users', req.url = '/users?sort=asc', req.originalUrl = '/api/users?sort=asc'.
Coding Challenges
Challenge 1 — req Deep Dive
Build an app with three routes that exercise the full req API. GET /inspect — returns a JSON snapshot of req.method, req.path, req.originalUrl, req.hostname, req.ip, req.protocol, all request headers, and all query params. POST /content-check — uses req.is() to detect whether the body is JSON or form-encoded; if JSON, echoes req.body; if urlencoded, echoes req.body; if neither, returns 415. GET /negotiate — uses req.accepts(['json', 'html', 'text']) to respond with the appropriate format: JSON object, an HTML paragraph, or plain text; returns 406 if none match. Write supertest tests covering each branch of each route — include tests that send an Accept header requesting HTML and verify the response Content-Type.
Challenge 2 — res Deep Dive
Build an app demonstrating the full res API. GET /headers-demo — sets three custom response headers (X-App-Version, Cache-Control: no-store, X-Request-Id from a crypto.randomUUID()), then responds with JSON. GET /redirect-demo — accepts a ?to= query param; if missing, 400; if the value is not in an allowlist ['/safe-a', '/safe-b'], 403; otherwise, 302 redirect. GET /safe-a and GET /safe-b each return a simple JSON response. GET /locals-demo — mount a middleware that writes res.locals.startTime and res.locals.requestId; the route handler reads both from res.locals and returns them in the response. Write supertest tests verifying all headers, the redirect chain, the 403 on bad destination, and that res.locals values appear in the response.
Challenge 3 — Cookie Jar
Build a minimal cookie-based "session" using cookie-parser with a signed cookie. Four routes: POST /login — expects { username, password } in the body; validates against a hardcoded map ({ alice: 'pass1', bob: 'pass2' }); on success, sets a signed session cookie containing the username (httpOnly: true, maxAge: 600_000) and returns { ok: true }; on failure, 401. GET /profile — reads req.signedCookies.session; if absent or false (tampered), 401; otherwise returns { username }. POST /logout — clears the session cookie and returns { loggedOut: true }. Write supertest tests using a cookie jar (supertest's agent() maintains cookies between requests) to simulate: login → profile → logout → profile returns 401 after logout. Also test that a manually crafted (unsigned) cookie value on /profile returns 401.