InteractiveFrameworks

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

  1. Parse cookies into req.cookies.
  2. GET /cats sorts by the sort cookie when it is name or age, by id otherwise, and answers JSON or CSV (id,name,age, then one line per cat, each ending in a newline) as the Accept header prefers, or 406 with { "error": "Available as application/json or text/csv" }.
  3. POST /preferences takes { "sort": "name" | "age" } as JSON only (415 with { "error": "Send preferences as application/json" } otherwise), refuses any other sort with 400 and { "error": "sort must be name or age" }, and otherwise sets a sort cookie, HttpOnly and SameSite=Lax, and answers 204.
  4. DELETE /preferences clears the cookie and answers 204.
  5. GET /cats/oldest redirects, temporarily, to /cats/<id of the oldest cat>.

When it fails

  • Cannot read properties of undefined (reading 'sort'): req.cookies does not exist without cookieParser().
  • "Neither" answers an HTML page: res.format() has no default, so Express made the 406 itself.
  • The cookie's Set-Cookie lacks HttpOnly or SameSite=Lax: the options were not passed.
  • GET /cats/oldest answers Cat oldest not found: it is declared after /cats/:id.

Remember

  • res.format() negotiates the representation, sets the content type and Vary; give it a default.
  • req.is(type) checks a body's Content-Type; false and null both mean no.
  • cookie-parser reads cookies into req.cookies; res.cookie() and res.clearCookie() write them, httpOnly and sameSite on.
  • 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.