Same Endpoint, Multiple Formats
Content Negotiation
Return JSON to API clients, HTML to browsers, CSV to spreadsheets — all from the same URL.
What you'll learn
- Use req.accepts() and res.format()
- Choose between formats
- Send a 406 when nothing matches
A single endpoint can serve multiple representations. The client
says what it wants via Accept; the server picks the best match.
req.accepts()
app.get("/users/42", (req, res) => {
if (req.accepts("html")) {
return res.render("user", { user });
}
if (req.accepts("json")) {
return res.json(user);
}
res.status(406).end();
}); req.accepts(type) returns the matched MIME type (or false).
res.format() — The Cleaner API
app.get("/reports/q3", (req, res) => {
const report = await fetchReport();
res.format({
"application/json": () => res.json(report),
"text/csv": () => {
res.set("content-type", "text/csv");
res.send(toCSV(report));
},
"text/html": () => res.render("report", { report }),
default: () => res.status(406).end(),
});
}); Express picks the handler that best matches the request’s Accept.
When To Use It
- APIs serving both browsers and clients — JSON for clients, HTML pages for browsers
- Reports — same URL gives JSON, CSV, PDF
- Backwards compatibility — old clients ask for XML, new for JSON
When Not To
For pure JSON APIs, content negotiation is overkill. Just return JSON.
req.is() — Inbound Content-Type
req.accepts() checks what the client wants. req.is() checks
what the client sent:
app.post("/upload", (req, res) => {
if (req.is("multipart/form-data")) {
return handleMultipart(req, res);
}
if (req.is("application/json")) {
return handleJSON(req, res);
}
res.status(415).end(); // Unsupported Media Type
}); Status Codes To Know
- 406 Not Acceptable — we don’t have what the client asked for
- 415 Unsupported Media Type — we don’t accept what the client sent