Skip to content
Node.js nd http 5 min read

The Request & Response Objects

Every request handler in Node’s http module receives two objects: a request and a response. The request is an IncomingMessage — a readable stream carrying the method, URL, headers, and body sent by the client. The response is a ServerResponse — a writable stream you use to send back a status line, headers, and a body. Understanding the shape and lifecycle of these two objects is the foundation for building anything from a tiny API to a full web framework.

The request object (IncomingMessage)

When the server’s 'request' event fires, the first argument is an http.IncomingMessage. It is a Readable stream, which means you can pipe it, listen for 'data'/'end', or consume it with async iteration. Beyond the body, it exposes metadata parsed from the incoming HTTP message.

The most commonly used properties are:

PropertyTypeDescription
req.methodstringHTTP verb, e.g. "GET", "POST" (always uppercase).
req.urlstringRequest target path and query, e.g. "/users?page=2".
req.headersobjectLowercased header names mapped to values.
req.httpVersionstringProtocol version, e.g. "1.1".
req.socketnet.SocketUnderlying connection (remote address, TLS info).

Note that req.url is only a path, never a full URL. To parse the query string and path safely, construct a URL using the host header as a base:

import { createServer } from 'node:http';

const server = createServer((req, res) => {
  const { pathname, searchParams } = new URL(req.url, `http://${req.headers.host}`);

  console.log(`${req.method} ${pathname}`);
  console.log('User-Agent:', req.headers['user-agent']);

  res.end(`Path: ${pathname}, page: ${searchParams.get('page') ?? '1'}`);
});

server.listen(3000, () => console.log('Listening on http://localhost:3000'));

Output: (after curl "http://localhost:3000/users?page=2")

GET /users
User-Agent: curl/8.7.1

Header names in req.headers are always lowercased by Node, so always read them as req.headers['content-type'], never 'Content-Type'. For headers that can legally appear multiple times (like set-cookie), use req.headersDistinct to get arrays.

Reading a streamed request body

The body of a request is not buffered for you — it arrives as a stream of chunks. For small payloads you can collect those chunks, but the streaming model lets you process gigabytes without holding them all in memory. The cleanest modern approach is for await...of, since IncomingMessage is an async iterable.

import { createServer } from 'node:http';

const server = createServer(async (req, res) => {
  if (req.method !== 'POST') {
    res.writeHead(405, { 'Content-Type': 'text/plain' });
    return res.end('Method Not Allowed');
  }

  const chunks = [];
  let total = 0;

  try {
    for await (const chunk of req) {
      total += chunk.length;
      if (total > 1_000_000) {            // guard against oversized uploads
        res.writeHead(413).end('Payload Too Large');
        req.destroy();
        return;
      }
      chunks.push(chunk);
    }
  } catch (err) {
    res.writeHead(400).end('Read error');
    return;
  }

  const body = Buffer.concat(chunks).toString('utf8');
  console.log(`Received ${total} bytes`);

  res.writeHead(200, { 'Content-Type': 'application/json' });
  res.end(JSON.stringify({ received: total }));
});

server.listen(3000);

Output: (after curl -X POST --data '{"name":"Ada"}' http://localhost:3000)

Received 14 bytes

Because the request is a real stream, you can also pipe it directly — for example, streaming an upload to disk with req.pipe(fs.createWriteStream('upload.bin')) without ever buffering it.

The response object (ServerResponse)

The second handler argument is an http.ServerResponse, a Writable stream. You control the status code and headers first, then write the body, and finally signal completion with end(). Once a single byte of the body (or writeHead) has been sent, the headers are flushed and can no longer be changed.

MethodPurpose
res.statusCode / res.statusMessageSet the response status before sending.
res.setHeader(name, value)Set or replace a header (before sending).
res.getHeader(name)Read a header you’ve staged.
res.writeHead(status, [headers])Send the status line and headers in one call.
res.write(chunk)Write a body chunk; flushes headers if not yet sent.
res.end([chunk])Send the final chunk (optional) and finish the response.

There are two equivalent ways to set headers. setHeader stages them individually and is convenient when building a response across several steps; writeHead sends the status and a header object together in one shot. You can mix them — anything staged with setHeader is merged when writeHead runs.

import { createServer } from 'node:http';

const server = createServer((req, res) => {
  // Staged incrementally:
  res.setHeader('X-Powered-By', 'DevCraftly');
  res.setHeader('Cache-Control', 'no-store');

  // Status line + content type sent together; staged headers are merged in:
  res.writeHead(200, { 'Content-Type': 'text/html; charset=utf-8' });

  res.write('<h1>Hello</h1>');
  res.write('<p>Streamed in two writes.</p>');
  res.end();
});

server.listen(3000);

Output: (response headers from curl -i http://localhost:3000)

HTTP/1.1 200 OK
X-Powered-By: DevCraftly
Cache-Control: no-store
Content-Type: text/html; charset=utf-8
Transfer-Encoding: chunked

Notice the Transfer-Encoding: chunked header: because we called write multiple times without setting a Content-Length, Node automatically uses chunked transfer encoding. If you know the full body up front, set Content-Length (or just pass the whole body to res.end(body)) so the client gets the length immediately.

Always call res.end() exactly once per request. Forgetting it leaves the client hanging until a timeout; calling it twice (or writing after it) throws ERR_STREAM_WRITE_AFTER_END. Returning early from a branch is the usual cause — return res.end(...) is a safe habit.

Best practices

  • Build a URL from req.url plus the host header instead of hand-parsing strings, so query parsing and decoding are handled correctly.
  • Treat req.headers keys as lowercase, and reach for req.headersDistinct when a header may legally repeat.
  • Never trust the request body’s size — enforce a byte cap while streaming to avoid memory-exhaustion attacks.
  • Set the status code and all headers before the first res.write; once the body flows, headers are locked.
  • Prefer res.end(body) for known payloads so Node can send an accurate Content-Length; reserve chunked writes for genuinely streamed output.
  • Always finish with a single res.end(), even on error paths, and pair it with an appropriate status code.
Last updated June 14, 2026
Was this helpful?