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.jsonponly 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 theRefererheader. Express 5 removed the magic"back"keyword — readreq.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
| Goal | Method | Sets Content-Type |
|---|---|---|
| HTML or pre-formatted string | res.send | text/html |
| JSON data API | res.json | application/json |
| JSON for legacy cross-origin | res.jsonp | text/javascript (with callback) |
| Browser navigation | res.redirect | n/a (sets Location) |
| Inline file (view in browser) | res.sendFile | from extension |
| File download prompt | res.download | from extension + attachment |
| Server-rendered page | res.render | text/html |
Best Practices
- Use
res.jsonfor data and reserveres.sendfor HTML or pre-built strings. - Always end the response exactly once and
returnfrom guard clauses so later code cannot send again. - Give
res.sendFileandres.downloadan absolute path or arootoption to block path traversal. - Prefer
301for permanent redirects and303after 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.jsonpfor new APIs; enable CORS instead and keep JSONP only for clients you cannot update.