NodeJSThe Response Object

The Response Object

Express's res object wraps Node's raw ServerResponse with helpers that make sending replies a one-liner: res.json(), res.send(), res.status(), res.redirect(), and more. They infer content types, serialize data, and chain fluently. This page covers them all — and the one rule that governs every response: send exactly one.

The core senders

Method

Sends

res.json(obj)

JSON — sets application/json, stringifies

res.send(body)

String/Buffer/object — infers Content-Type

res.status(code)

Sets status code (chainable, sends nothing)

res.sendStatus(code)

Sets status + sends its standard text

res.redirect([code,] url)

Redirect (302 by default)

res.sendFile(path)

Stream a file with correct headers

res.render(view, data)

Render a template to HTML

res.end()

End with no body (e.g. 204)

json and send

JS
res.json({ id: 1, name: 'Ada' })   // Content-Type: application/json
res.send('<h1>Hello</h1>')          // Content-Type: text/html
res.send(Buffer.from([1, 2, 3]))    // Content-Type: application/octet-stream
res.send({ ok: true })              // object → JSON (same as res.json)
`res.json()` over `res.send()` for APIs
`res.send` infers the type from the argument — handy, but for an API endpoint prefer the explicit `res.json()` so the `application/json` content type is guaranteed regardless of the value (even `null` or a number serialize correctly). `res.send` is best for HTML/text; `res.json` for data.
Chaining status with the body

JS
res.status(201).json({ id: 42 })          // 201 Created + JSON
res.status(404).json({ error: 'Not found' })
res.status(204).end()                      // no body

res.sendStatus(403)                        // → 403 + body 'Forbidden'
`res.status()` only SETS the code — it doesn't send
`res.status(404)` alone leaves the request hanging; it just sets the status, returning `res` so you chain a sender: `res.status(404).json(...)`. Don't confuse it with `res.sendStatus(404)`, which sets the code *and* sends the status text as the body. A lone `res.status(...)` with nothing after it is a common "why is my request timing out?" bug.
The cardinal rule: one response per request
Sending two responses throws ERR_HTTP_HEADERS_SENT
Each request gets **exactly one** response. Call `res.json()` (or any sender) twice — or send after already responding — and Express throws `Cannot set headers after they are sent to the client`. The usual cause is forgetting to `return` after an early response, so the handler continues and sends again. Always `return` when you respond conditionally.

JS
// ✗ Bug — both run, second send crashes
app.get('/x', (req, res) => {
  if (!ok) res.status(400).json({ error: 'bad' })   // no return!
  res.json(data)                                      // runs anyway → throws
})

// ✓ Return after responding
app.get('/x', (req, res) => {
  if (!ok) return res.status(400).json({ error: 'bad' })
  res.json(data)
})
Redirects

JS
res.redirect('/login')                 // 302 (temporary) by default
res.redirect(301, '/new-home')         // 301 permanent
res.redirect('https://example.com')    // absolute URL
res.redirect('back')                   // back to the Referer (or '/')
Choose 301 vs 302 deliberately
`302` (temporary) is the default and tells browsers/caches "this might change back." Use `301` (permanent) only when the move is truly permanent — browsers and search engines **cache** 301s aggressively, so a mistaken 301 can be very sticky to undo. See [status codes](/nodejs/http-status-codes) for the redirect family.
Setting headers, cookies, and content type

JS
res.set('X-Custom', 'value')                   // one header
res.set({ 'X-A': '1', 'X-B': '2' })            // several
res.type('application/json')                   // shortcut for Content-Type

// Cookies (Express helper — no manual Set-Cookie string):
res.cookie('sessionId', 'abc', {
  httpOnly: true, secure: true, sameSite: 'strict', maxAge: 3_600_000,
})
res.clearCookie('sessionId')
Set headers and cookies BEFORE sending the body
As with raw Node, headers (including `Set-Cookie`) must be set before the body is sent — once `res.json()`/`res.send()` fires, headers are flushed and locked. Order your code: status → headers/cookies → body. And always set `httpOnly`/`secure`/`sameSite` on session cookies, per the [cookie security rules](/nodejs/http-headers).
Sending files and downloads

JS
import { join } from 'node:path'

// Stream a file inline (sets Content-Type from the extension):
res.sendFile(join(import.meta.dirname, 'public', 'report.pdf'))

// Prompt a download with a filename:
res.download(join(import.meta.dirname, 'files', 'data.csv'), 'export.csv')
`res.sendFile` needs an absolute path
`res.sendFile` requires an absolute path (or a `root` option) — pass a relative path and it throws. Build it from `import.meta.dirname` to anchor to the module, avoiding the [cwd trap](/nodejs/file-paths). `res.download` is the same but adds a `Content-Disposition: attachment` header so the browser saves rather than displays the file.
Quick reference

Goal

Call

Send JSON

res.json(obj)

Send with status

res.status(code).json(obj)

Empty success

res.status(204).end()

Redirect

res.redirect(url)

Set a cookie

res.cookie(name, value, opts)

Stream a file

res.sendFile(absPath)

Force download

res.download(absPath, name)

Next
Split your routes into clean, mountable modules: [Express Router & Modular Routes](/nodejs/express-router).