Sending Responses

Exactly one response must leave each request, and the method you pick decides the status line, the Content-Type and whether a validator is attached.

Ways an Express handler ends a requestJavaScript
app.get('/send/string', (req, res) => res.send('<p>Hello</p>'));
app.get('/send/object', (req, res) => res.send({ ok: true, count: 2 }));
app.get('/send/buffer', (req, res) => res.send(Buffer.from([0x50, 0x4b, 0x03])));
app.get('/send/status', (req, res) => res.sendStatus(204));
app.get('/send/created', (req, res) =>
  res.status(201).location('/books/42').json({ id: 42 }));
Output
GET /send/string   200 OK  text/html; charset=utf-8  ETag: W/"c-4a+EvIM3a0+SSNp45gaEvHsev3E"
GET /send/object   200 OK  application/json; charset=utf-8
GET /send/buffer   200 OK  application/octet-stream
GET /send/status   204 No Content
GET /send/created  201 Created  Location: /books/42

res.send() guesses the type from its argument: a string becomes HTML, an object or array JSON, a Buffer application/octet-stream. That guess is why res.json() is the better habit in an API — it always produces JSON, even for a string, a number or null.

Methods that finish a response
Method What it does
res.status(code) Sets the status, returns res for chaining
res.send(body) Sends a string, object or Buffer, guessing type
res.json(obj) Serializes to JSON, sets application/json
res.sendStatus(code) Sets the status, sends its standard text
res.end() Node's raw finish: no body, type or ETag
res.headersSent true once the head is out; nothing can change

Express 5 24,430 tightened these signatures: res.send(body, status) and res.json(obj, status) are gone, as is res.send(404) with a bare number — write res.status(404).json(...) or res.sendStatus(404). res.status() now accepts only integers from 100 to 999.

For res.send() and res.json() Express hashes the body and attaches the weak ETag above, so a client repeating the request with a matching If-None-Match receives 304 Not Modified and no body. The saving is bandwidth, not work: your handler still built that body. app.set('etag', false) turns it off.