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.
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 }));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.
| 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.