Response Object

beginner express response

res is the outbox. Whatever we want to send back — JSON, HTML, a file, a redirect — we do it through res. In simple language: req is what came in, res is what we send out.

The golden rule: we can only send one response per request. Call res.send() twice and Express throws “Cannot set headers after they are sent.” Always return after sending.

res — what we send back
Send body
.send() .json() .sendFile() .render()
Set status
.status() .sendStatus()
Set headers
.set() .type() .cookie()
Navigate
.redirect() .location()

res.send() — the universal sender

Sends whatever we give it and sets Content-Type based on the type.

app.get("/text", (req, res) => res.send("hello"));           // text/html
app.get("/obj",  (req, res) => res.send({ ok: true }));      // application/json
app.get("/buf",  (req, res) => res.send(Buffer.from("hi"))); // application/octet-stream

Express figures out the content type. For JSON specifically, prefer res.json().

res.json() — for APIs

app.get("/api/users/:id", (req, res) => {
  res.json({ id: req.params.id, name: "Manish" });
});

Same as res.send() for objects, but it also handles null and primitives correctly as JSON. Use this for every JSON API endpoint — it’s clearer intent.

{ "id": "42", "name": "Manish" }

res.status() — set the status code

Returns res, so we chain it:

app.post("/users", (req, res) => {
  if (!req.body.email) {
    return res.status(400).json({ error: "email is required" });
  }
  const user = createUser(req.body);
  res.status(201).json(user);   // 201 Created
});

Defaults to 200. For empty responses, res.sendStatus(204) sets status AND sends the standard status text.

res.redirect() — send the browser elsewhere

app.get("/old-page", (req, res) => {
  res.redirect(301, "/new-page");   // 301 = permanent
});

app.post("/login", (req, res) => {
  // ...auth...
  res.redirect("/dashboard");        // defaults to 302
});

301 is permanent (browser caches it forever), 302/303 are temporary. For POST-redirect-GET pattern, use 303.

res.sendFile() — stream a file from disk

const path = require("path");

app.get("/download/report.pdf", (req, res) => {
  res.sendFile(path.join(__dirname, "files", "report.pdf"));
});

The path must be absolute (or pass { root: ... }). Express sets Content-Type based on file extension and streams it efficiently. For forced download:

app.get("/download/:file", (req, res) => {
  res.download(path.join(__dirname, "files", req.params.file));
  // Sets Content-Disposition: attachment so browser saves instead of viewing
});

res.set() — set headers

res.set("X-Powered-By", "Coffee");
res.set({ "Cache-Control": "no-store", "X-Request-Id": "abc123" });

// Shortcut for content type
res.type("text/csv");
res.type("json");   // application/json

Headers must be set before res.send(). After we send, headers are locked.

Cookies

res.cookie("session", "abc123", {
  httpOnly: true,
  secure: true,
  sameSite: "lax",
  maxAge: 24 * 60 * 60 * 1000,  // 1 day in ms
});

res.clearCookie("session");

httpOnly prevents JS access (XSS protection), secure requires HTTPS, sameSite blocks CSRF.

Chaining — everything returns res

This is the Express magic — every setter returns res, so we chain:

res
  .status(201)
  .set("Location", `/users/${user.id}`)
  .type("json")
  .json(user);

Reads top-to-bottom like a recipe.

A realistic endpoint

app.post("/api/users", async (req, res, next) => {
  try {
    if (!req.body.email) {
      return res.status(400).json({ error: "email required" });
    }

    const user = await db.createUser(req.body);

    res
      .status(201)
      .set("Location", `/api/users/${user.id}`)
      .json(user);
  } catch (err) {
    next(err);
  }
});

Status, location header, JSON body — all in one chain. That’s res in a nutshell.