NodeJSRequest & Response Objects

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

req.method

HTTP verb — 'GET', 'POST', 'PUT', 'DELETE'…

req.url

Path + query string (NOT the full URL)

req.headers

All request headers as a lowercased object

req.httpVersion

'1.1', '2.0', etc.

req.socket

The underlying TCP socket (remote address, etc.)

(stream) data/end

The request body — must be read

`req.url` is path + query only — there's no `req.path` or `req.query` in raw Node
Raw `http` gives you only `req.url` as a string like `/users?id=5` — no parsed `path`, `query`, or `params`. You parse it yourself with the `URL` class. (Express *adds* `req.path`, `req.query`, and `req.params` — they are framework conveniences, not core Node properties. Don't reach for them in raw-`http` code.)
Reading headers

JS
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)
Header names are always lowercase
Node normalizes incoming header names to lowercase, so always access `req.headers['content-type']`, never `'Content-Type'`. HTTP header names are case-insensitive by spec; Node just picks lowercase as canonical. Most headers are single strings, but ones that can legitimately repeat (e.g. `set-cookie`) come through as arrays.
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

JS
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'))
Always cap body size — unbounded reads are a DoS vector
If you accumulate the body without a limit, a client can stream gigabytes and crash your process (out of memory). Enforce a maximum and reject (HTTP 413) when exceeded — and call `req.destroy()` to stop reading. Body-parsing middleware (and frameworks) apply such limits by default; raw Node does not, so it's on you.
The response object (ServerResponse)

Property / method

Purpose

res.statusCode = n

Set the status (default 200)

res.setHeader(name, value)

Set one response header

res.writeHead(code, headers)

Set status + headers together

res.write(chunk)

Send a piece of the body (can call repeatedly)

res.end([chunk])

Finish the response (required)

res.headersSent

Boolean — have headers gone out yet?

The cardinal rule: headers before body
You cannot set headers after writing body data
Once the first byte of the body is sent (the first `res.write()` or `res.end(data)`), the headers are flushed and **locked**. Calling `res.setHeader()` or `res.writeHead()` afterward throws `ERR_HTTP_HEADERS_SENT`. This reflects the wire protocol: headers physically precede the body. Set every header — status, content-type, cookies — *before* you write any content.

JS
// ✗ 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:

JS
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)
`pipeline(readable, res)` is the safe way to stream a response
Piping a file to `res` streams it in chunks with proper backpressure — memory stays flat for any file size. Use `pipeline` (not bare `.pipe()`) so a read error destroys the response cleanly instead of leaving a half-sent reply. This is how you serve large downloads or media without exhausting RAM.
Detecting a disconnected client

JS
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)
The `close` event lets you abort wasted work
If a client navigates away mid-request, the `req`/socket `close` event fires. For expensive handlers (big DB queries, large generated reports), listen for it and cancel the work — otherwise you burn CPU producing a response no one will receive. Check `res.writableEnded` to distinguish a normal finish from an early disconnect.
Next
The verbs that define what a request wants to do: [HTTP Methods](/nodejs/http-methods).