Skip to content
Express.js ex requests 4 min read

Sending Responses

Express gives you a family of response methods on res, each tuned to a particular kind of payload — plain text, JSON, HTML templates, files, attachments, or redirects. Picking the right one matters because each sets a sensible Content-Type, applies the correct caching and disposition headers, and ends the response for you. This page walks through every sender so you can match the method to the content and stop hand-rolling headers.

res.send for general bodies

res.send() is the catch-all sender. It inspects the argument’s type, sets a matching Content-Type, computes Content-Length, handles conditional ETag/304 responses, and ends the cycle.

const express = require("express");
const app = express();

app.get("/text", (req, res) => {
  res.send("Hello, world"); // text/html
});

app.get("/data", (req, res) => {
  res.send({ id: 1, name: "Ada" }); // application/json
});

app.listen(3000);

A String becomes text/html, an object or array is serialized as JSON, and a Buffer becomes application/octet-stream. Because a bare string defaults to HTML, send plain text with res.type("text/plain").send(value) to avoid an accidental XSS surface.

res.json for data APIs

When the body is data, prefer res.json(). It always runs the value through JSON.stringify, forces Content-Type: application/json, and honors app settings like json spaces and a custom json replacer. Unlike res.send, it serializes primitives correctly — res.json("ok") and res.json(null) produce valid JSON rather than HTML.

app.get("/users/:id", async (req, res) => {
  const user = await db.findUser(req.params.id);
  if (!user) return res.status(404).json({ error: "not found" });
  res.json(user);
});

Output:

{ "id": "42", "name": "Ada", "role": "admin" }

res.jsonp for padded JSON

res.jsonp() behaves like res.json but, when the request includes a callback query parameter, wraps the JSON in a function call for legacy cross-origin scripts. The default parameter name is callback, configurable via the jsonp callback name app setting.

app.set("jsonp callback name", "cb");

app.get("/weather", (req, res) => {
  res.jsonp({ tempC: 21 });
});

A request to /weather?cb=show responds with Content-Type: text/javascript:

Output:

/**/ typeof show === 'function' && show({"tempC":21});

Tip: JSONP predates CORS and bypasses the same-origin policy by executing remote code. For modern APIs use CORS instead; reach for res.jsonp only to support old clients you cannot change.

res.redirect for navigation

res.redirect() sends a Location header and a redirect status — 302 Found by default. Pass a status first to override it, for example 301 for permanent moves or 303 See Other after a POST.

app.post("/login", express.urlencoded({ extended: true }), (req, res) => {
  // authenticate...
  res.redirect("/dashboard"); // 302 to a relative path
});

app.get("/old-docs", (req, res) => {
  res.redirect(301, "https://devcraftly.com/docs");
});

Paths can be relative to the mount point ("dashboard"), root-relative ("/dashboard"), or fully qualified URLs.

Warning: Express 4 supported res.redirect("back") to bounce to the Referer header. Express 5 removed the magic "back" keyword — read req.get("Referrer") || "/" and pass an explicit URL.

res.sendFile for serving files

res.sendFile() streams a file from disk, deriving Content-Type from the extension and setting Last-Modified plus ETag for caching. The path must be absolute, or you must supply a root option — a guard against path-traversal attacks.

const path = require("path");

app.get("/report", (req, res) => {
  res.sendFile("report.pdf", { root: path.join(__dirname, "files") }, (err) => {
    if (err) res.status(err.statusCode || 500).end();
  });
});

The optional callback fires when the transfer completes or errors, letting you log or recover. For serving a whole directory of static assets, use express.static middleware instead of calling sendFile per route.

res.download for attachments

res.download() is sendFile plus a Content-Disposition: attachment header, prompting the browser to save rather than render the file. The second argument overrides the suggested filename.

app.get("/invoice/:id", async (req, res) => {
  const file = path.join(__dirname, "invoices", `${req.params.id}.pdf`);
  res.download(file, "invoice.pdf", (err) => {
    if (err && !res.headersSent) res.status(404).end();
  });
});

res.render for HTML templates

res.render() compiles a view through the configured template engine and sends the resulting HTML. Configure the views directory and view engine once, then pass a locals object to each call.

app.set("view engine", "ejs");
app.set("views", path.join(__dirname, "views"));

app.get("/profile", (req, res) => {
  res.render("profile", { name: "Ada", role: "admin" });
});

res.render defaults to text/html and a 200 status. To change the status, chain it: res.status(404).render("404").

Choosing the right method

GoalMethodSets Content-Type
HTML or pre-formatted stringres.sendtext/html
JSON data APIres.jsonapplication/json
JSON for legacy cross-originres.jsonptext/javascript (with callback)
Browser navigationres.redirectn/a (sets Location)
Inline file (view in browser)res.sendFilefrom extension
File download promptres.downloadfrom extension + attachment
Server-rendered pageres.rendertext/html

Best Practices

  • Use res.json for data and reserve res.send for HTML or pre-built strings.
  • Always end the response exactly once and return from guard clauses so later code cannot send again.
  • Give res.sendFile and res.download an absolute path or a root option to block path traversal.
  • Prefer 301 for permanent redirects and 303 after a successful POST so the browser issues a clean GET.
  • Set the status before the sender — res.status(201).json(created) — since senders flush headers immediately.
  • Avoid res.jsonp for new APIs; enable CORS instead and keep JSONP only for clients you cannot update.
Last updated June 14, 2026
Was this helpful?