Request and response
The request and the response in detail: answer JSON or CSV with res.format(), refuse a body that is not JSON with req.is(), remember a preference in a cookie with cookie-parser, and redirect to a resource that can move.
What you'll learn
- Answer in the representation the client prefers with res.format(), including a default for none
- Check a request body's type with req.is(), and read and set cookies with cookie-parser, res.cookie() and res.clearCookie()
- Redirect with res.redirect(), choosing between a temporary and a permanent move
The first lessons used req and res for the essentials: parameters, a body, a status, JSON. Both carry a good deal more, and Express wraps the fiddly parts of HTTP in helpers that are worth knowing by name, because the hand-written versions are where bugs live. This lesson covers four of them: choosing a representation, checking what a body claims to be, cookies, and redirects. The HTTP course explains the protocol side of each; here it is the Express side.
Choosing a representation: res.format()
Parsing an Accept header by hand means splitting on commas, weighing q-values and handling wildcards. res.format() does all of it: give it one function per media type, and it runs the one the client prefers:
app.get('/owners', (req, res) => {
res.format({
'application/json': () => res.json(owners),
'text/html': () => res.send(`<ul>${owners.map((o) => `<li>${o.name}</li>`).join('')}</ul>`),
default: () => res.status(406).json({ error: 'JSON or HTML only' }),
});
});
Before calling the handler it sets the response's Content-Type to the type it matched, and adds Vary: Accept for caches. Without a default, a request that accepts none of the types becomes a 406 through Express's error handling, which here answers with its HTML page; a default handler answers in the API's own shape instead.
What does the body claim to be? req.is()
req.is('application/json') compares the request's Content-Type with a type (parameters such as a charset are ignored, and wildcards like 'json' or 'text/*' work too). It returns the matching type, false when the body is something else, and null when there is no body at all. Both false and null mean "not JSON", so if (!req.is('application/json')) is the check for a route that only reads JSON.
Cookies: cookie-parser, res.cookie(), res.clearCookie()
Express does not parse the Cookie header on its own. The cookie-parser middleware does, and fills req.cookies with one entry per cookie:
import cookieParser from 'cookie-parser';
app.use(cookieParser());
app.get('/theme', (req, res) => {
res.json({ theme: req.cookies.theme ?? 'light' });
});
Setting a cookie is res.cookie(name, value, options), which writes the Set-Cookie header. The options are the cookie's attributes, and two of them should be on by default: httpOnly: true keeps the page's JavaScript from reading it, and sameSite: 'lax' keeps other sites from sending it along with their requests. maxAge (in milliseconds) or expires make it outlive the browser session; without either it is a session cookie. res.clearCookie(name) sends the same cookie back already expired, which is how a server deletes one.
A cookie is state the client carries for you. A preference like a sort order is a good fit: small, harmless if tampered with, and useful on every request. A user id is not, unless it is signed; cookie-parser can do that too (cookieParser(secret) and signed: true).
Redirects: res.redirect()
res.redirect(url) answers 302 Found with a Location header, and res.redirect(status, url) picks the code. The choice is the one from the HTTP course: 301 or 308 for a move that is permanent, which clients and caches may remember; 302 or 307 for one that is not. A link that means "whichever is newest" must be temporary, because the answer will change; making it permanent would let a cache keep pointing at yesterday's newest forever.
Your task
- Parse cookies into
req.cookies. GET /catssorts by thesortcookie when it isnameorage, by id otherwise, and answers JSON or CSV (id,name,age, then one line per cat, each ending in a newline) as theAcceptheader prefers, or406with{ "error": "Available as application/json or text/csv" }.POST /preferencestakes{ "sort": "name" | "age" }as JSON only (415with{ "error": "Send preferences as application/json" }otherwise), refuses any other sort with400and{ "error": "sort must be name or age" }, and otherwise sets asortcookie,HttpOnlyandSameSite=Lax, and answers204.DELETE /preferencesclears the cookie and answers204.GET /cats/oldestredirects, temporarily, to/cats/<id of the oldest cat>.
When it fails
Cannot read properties of undefined (reading 'sort'):req.cookiesdoes not exist withoutcookieParser().- "Neither" answers an HTML page:
res.format()has nodefault, so Express made the 406 itself. - The cookie's
Set-CookielacksHttpOnlyorSameSite=Lax: the options were not passed. GET /cats/oldestanswersCat oldest not found: it is declared after/cats/:id.
Remember
res.format()negotiates the representation, sets the content type andVary; give it adefault.req.is(type)checks a body'sContent-Type;falseandnullboth mean no.cookie-parserreads cookies intoreq.cookies;res.cookie()andres.clearCookie()write them,httpOnlyandsameSiteon.res.redirect()is a 302; pass the status for anything else.
Stuck? Show a hint
app.use(cookieParser()) fills req.cookies. res.format({ 'application/json': () => …, 'text/csv': () => …, default: () => … }) runs one handler by the Accept header and sets Vary. req.is('application/json') is the content type when it matches and false or null when it does not. res.cookie(name, value, { httpOnly: true, sameSite: 'lax' }) and res.clearCookie(name) write the Set-Cookie header. res.redirect(url) is a 302.