Sending responses

Express · Essentials · 11 min

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 with Content-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 returns res, 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; use req.get("Referrer") || "/".
  • res.sendFile(absolutePath) and res.download(path, filename) — stream a file with the right headers; use the root option 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.json and then falling through to another res.send throws "Cannot set headers after they are sent". Use return res.json(...) to stop the handler.
  • Wrong status codes. 200 for success with a body, 201 for created, 204 for success without a body, 400 for invalid input, 401 unauthenticated, 403 forbidden, 404 not found, 409 conflict, 500 for your bugs.
  • Express 5 removed the old two-argument forms such as res.json(obj, 201) and res.send(200); always use res.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); use return to 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", no res.json(obj, status)

Watch out for

  • Forgetting return after an early response, then sending a second one.

Check yourself

  1. A handler calls res.status(404).json({...}) inside an if, without return, and later res.json(item). What happens when the if runs?

Sign in to answer, see the explanations, and get this lesson back as a quick review later.

Official docs: expressjs.com/en/api/response