Request & Response Objects
Every HTTP handler receives two objects: req (an IncomingMessage) and res (a ServerResponse). They're not plain data bags — req is a readable stream and res is a writable stream, which explains why bodies must be read and why headers must be sent before data. This page maps out both objects completely so nothing about them stays mysterious.
The request object (IncomingMessage)
Property / method | What it gives you |
|---|---|
| HTTP verb — 'GET', 'POST', 'PUT', 'DELETE'… |
| Path + query string (NOT the full URL) |
| All request headers as a lowercased object |
| '1.1', '2.0', etc. |
| The underlying TCP socket (remote address, etc.) |
(stream) | The request body — must be read |
Reading headers
createServer((req, res) => {
// Header NAMES are lowercased by Node; values are as-sent:
const type = req.headers['content-type']
const auth = req.headers['authorization']
const ua = req.headers['user-agent']
// Duplicate headers (like set-cookie) may be arrays:
const cookies = req.headers['cookie']
res.end('ok')
}).listen(3000)Reading the request body
The body streams in after the headers. Collect it from the readable stream — and guard the size, or a malicious client can exhaust your memory:
Read a body safely, with a size limit
function readBody(req, limit = 1e6) { // 1 MB cap
return new Promise((resolve, reject) => {
let size = 0
const chunks = []
req.on('data', (chunk) => {
size += chunk.length
if (size > limit) {
reject(new Error('Payload too large'))
req.destroy() // stop reading
return
}
chunks.push(chunk)
})
req.on('end', () => resolve(Buffer.concat(chunks)))
req.on('error', reject)
})
}
// Usage:
const raw = await readBody(req)
const data = JSON.parse(raw.toString('utf8'))The response object (ServerResponse)
Property / method | Purpose |
|---|---|
| Set the status (default 200) |
| Set one response header |
| Set status + headers together |
| Send a piece of the body (can call repeatedly) |
| Finish the response (required) |
| Boolean — have headers gone out yet? |
The cardinal rule: headers before body
// ✗ Throws ERR_HTTP_HEADERS_SENT
res.write('hello')
res.setHeader('X-Foo', 'bar') // too late!
// ✓ Headers first, then body
res.setHeader('X-Foo', 'bar')
res.writeHead(200)
res.write('hello')
res.end()Streaming a response
Because res is a writable stream, you can pipe a file (or any readable) straight to the client — streaming with backpressure instead of buffering the whole thing:
import { createReadStream } from 'node:fs'
import { pipeline } from 'node:stream/promises'
createServer(async (req, res) => {
res.writeHead(200, { 'Content-Type': 'video/mp4' })
// Stream the file; pipeline handles backpressure + errors:
await pipeline(createReadStream('movie.mp4'), res)
}).listen(3000)Detecting a disconnected client
createServer((req, res) => {
req.on('close', () => {
if (!res.writableEnded) {
console.log('client disconnected before we finished')
// stop expensive work — nobody's listening
}
})
res.end('ok')
}).listen(3000)