Correct status codes, headers and bodies are what clients actually consume from your API.
Every request ends with exactly one response. Express's res helpers make the common cases one line:
res.json(value)— serialise to JSON withContent-Type: application/json. The go-to for APIs.res.send(body)— strings become HTML, Buffers become binary, objects become JSON.res.status(code)— set the status; it returnsres, so chain it:res.status(201).json(item). In Express 5 it throws on invalid codes (non-integers, or outside 100–999).res.sendStatus(code)— status plus its standard text as body;res.status(204).end()for an empty response.res.set(name, value)/res.get— headers;res.type("csv")sets Content-Type by extension.res.redirect([status,] url)— 302 by default; use 301/308 for permanent moves. Express 5 removed the magic"back"value; usereq.get("Referrer") || "/".res.sendFile(absolutePath)andres.download(path, filename)— stream a file with the right headers; use therootoption when the name comes from the user.res.cookie(name, value, options)/res.clearCookie(name).res.render(view, data)— render a template if you configured a view engine.
Common mistakes:
- Responding twice. Calling
res.jsonand then falling through to anotherres.sendthrows "Cannot set headers after they are sent". Usereturn res.json(...)to stop the handler. - Wrong status codes.
200for success with a body,201for created,204for success without a body,400for invalid input,401unauthenticated,403forbidden,404not found,409conflict,500for your bugs. - Express 5 removed the old two-argument forms such as
res.json(obj, 201)andres.send(200); always useres.status().
Example
import express from "express";
import path from "node:path";
const app = express();
const notes = new Map([[1, { id: 1, text: "hi" }]]);
app.get("/notes/:id", (req, res) => {
const note = notes.get(Number(req.params.id));
if (!note) return res.status(404).json({ error: "note not found" }); // return!
res.json(note);
});
app.delete("/notes/:id", (req, res) => {
notes.delete(Number(req.params.id));
res.status(204).end(); // success, no body
});
app.get("/export.csv", (req, res) => {
res.type("csv").attachment("notes.csv"); // download headers
res.send([...notes.values()].map((n) => `${n.id},${n.text}`).join("\n"));
});
app.get("/old-home", (req, res) => res.redirect(301, "/"));
app.get("/logo", (req, res) => res.sendFile("logo.png", { root: path.join(import.meta.dirname, "public") }));
app.listen(3000);Remember
- Chain
res.status(code).json(data); usereturnto stop the handler - Send exactly one response per request
- Use precise status codes: 201 created, 204 no content, 4xx client errors
- Express 5:
res.redirect(status, url); no"back", nores.json(obj, status)
Watch out for
- Forgetting
returnafter an early response, then sending a second one.