One resource can have several representations. res.format() inspects Accept, runs the branch whose type wins, and — the part people forget by hand — sets Vary: Accept so caches key them separately. The second route stages headers and a cookie before sending a body.
app.get('/books/42', (req, res) => {
res.format({
'application/json': () => res.json(book),
'text/html': () => res.send(`<h1>${book.title}</h1>`),
'text/csv': () => res.type('csv').send('id,title\n42,Dune\n'),
default: () => res.status(406).send('Not Acceptable')
});
});
app.get('/headers', (req, res) => {
res.set({ 'Cache-Control': 'public, max-age=60', 'X-Total-Count': '137' });
res.append('Link', '</books?page=2>; rel="next"');
res.type('json').cookie('sid', 'a1b2c3', { maxAge: 900000, httpOnly: true });
res.json({ contentType: res.get('Content-Type'), sent: res.headersSent });
});$ curl -s -D - -H 'Accept: text/html' .../books/42
HTTP/1.1 200 OK Vary: Accept Content-Type: text/html; charset=utf-8
$ curl -s -D - -H 'Accept: application/xml' .../books/42
HTTP/1.1 406 Not Acceptable Vary: Accept
$ curl -s -D - .../headers
Cache-Control: public, max-age=60 X-Total-Count: 137
Link: </books?page=2>; rel="next" Content-Type: application/json; charset=utf-8
Set-Cookie: sid=a1b2c3; Max-Age=900; Path=/; Expires=Thu, 17 Sep 2026 17:54:52 GMT; HttpOnly
{"contentType":"application/json; charset=utf-8","sent":false}The format keys may be extensions (json, html) or full MIME types, and are tried in the order the client ranked them, not the order you wrote them. Without a default branch Express 24,430 raises its own 406 into your error middleware; supplying one keeps the payload in your control.
res.set() takes a name and value or a whole object, res.append() adds a value to a repeatable header, res.type() sets Content-Type from an extension, and res.get() reads it back. res.cookie() serializes Set-Cookie, converting maxAge from milliseconds to the seconds the header wants (900000 became Max-Age=900) and adding Expires and Path. res.clearCookie() re-sends the cookie with a 1970 expiry and, in Express 5, ignores any maxAge or expires; its path and domain must match those used to set it. Reading cookies back needs cookie-parser 2,030 (Setting and Reading Cookies).
The sent: false is the point: nothing had been written when res.json() serialized that object, because res.headersSent flips only as the head leaves — which is why header changes must precede the send. And res.vary(field) is the manual form of what res.format() did above, for a body that varies by header.