Content Negotiation and Media Types
Content negotiation is the mechanism by which an HTTP client and server agree on the best representation of a resource. The client advertises what formats it can accept via the Accept header; the server inspects that header and responds with the most appropriate Content-Type. Express does not implement full content negotiation automatically, but it gives you req.accepts() and res.format() to wire it up cleanly.
The Accept Header and MIME Types
The Accept request header lists the MIME types the client prefers, with optional quality values (q-factors) indicating priority. For example: Accept: application/json, text/html;q=0.9, */*;q=0.8. The server should honour the highest-priority type it can produce. Common MIME types in APIs include application/json, application/xml, text/csv, text/html, and application/pdf.
// Reading Accept header manually
app.get('/report', (req, res) => {
const preferred = req.accepts(['json', 'html', 'csv']);
if (preferred === 'json') {
res.json({ sales: 1200 });
} else if (preferred === 'html') {
res.send('<h1>Sales: 1200</h1>');
} else if (preferred === 'csv') {
res.setHeader('Content-Type', 'text/csv');
res.send('metric,value\nsales,1200');
} else {
res.status(406).json({ error: 'Not Acceptable' });
}
});res.format() — Declarative Content Negotiation
Express provides res.format() which takes an object keyed by MIME type and automatically picks the best match based on the Accept header. It throws a 406 error automatically if no match is found, which you can catch with a default key. This is cleaner than nested if-else logic and keeps negotiation intent visible at the route level.
app.get('/players/:id', async (req, res) => {
const player = await Player.findById(req.params.id);
res.format({
'application/json': () => {
res.json(player);
},
'text/html': () => {
res.render('player', { player });
},
'text/csv': () => {
res.setHeader('Content-Disposition', 'attachment; filename="player.csv"');
res.send(`id,name,runs\n${player.id},${player.name},${player.runs}`);
},
default: () => {
res.status(406).send('Not Acceptable');
}
});
});