Search across all documentation pages
10 examples to understand HTTP in Node.js with http.createServer - 7 basic and 3 intermediate.
These examples assume Node.js 24.18.0 (Active LTS) and TypeScript 5.6+ with ESM ("type": "module" in package.json).
mkdir http-basics-spike && cd http-basics-spike
npm init -y
npm pkg set type=module
npm install -D typescript@5.6 tsx @types/nodeFor framework-level patterns built on this foundation, see Middleware Pattern and Express Basics.
Every Node HTTP server starts with http.createServer and a request handler.
import { createServer } from "node:http";
const server = createServer((req, res) => {
res.writeHead(200, { "Content-Type": "text/plain" });
res.end("Hello from Node HTTP\n"
req is an IncomingMessage with method, url, and headersres is a ServerResponse - you must call res.end() or the client waits foreverwriteHead sets status and headers in one call; use it before the first writeRelated: Routing Without Frameworks - match paths without Express
Route by inspecting req.method and parsing req.url.
import { createServer } from "node:http";
import { parse } from "node:url";
const server = createServer((req, res) => {
const { pathname } = parse(req.url ?? "/",
req.url includes query string - use URL or parse from node:url for pathnamereq.path and req.queryHeaders arrive lowercase in Node 24. Access them via req.headers.
import { createServer } from "node:http";
const server = createServer((req, res) => {
const contentType = req.headers["content-type"] ?? "none";
const userAgent = req.headers["user-agent"
, in a single string valueX-Forwarded-For without proxy configuration - see Reverse Proxy AwarenessSet Content-Type: application/json and stringify your payload.
import { createServer } from "node:http";
const users = [{ id: 1, name: "Ada" }];
const server = createServer((req, res) => {
res.writeHead(200, {
"Content-Type"
charset=utf-8 for JSON with non-ASCII textJSON.stringify with a replacer for dates: date.toISOString()Collect data chunks from the request stream, then parse.
import { createServer } from "node:http";
async function readBody(req: import("node:http").IncomingMessage): Promise<string> {
const chunks: Buffer[] = [];
for await
express.json() and Fastify's built-in parser handle this with limitsUse meaningful status codes; clients and load balancers depend on them.
import { createServer } from "node:http";
const server = createServer((req, res) => {
if (req.method === "GET" && req.url === "/users/999") {
res.writeHead(404, { "Content-Type"
| Code | Meaning | When to use |
|---|---|---|
| 200 | OK | Successful GET, PUT, PATCH |
| 201 | Created | Successful POST that created a resource |
| 400 | Bad Request | Validation failure, malformed input |
| 404 | Not Found | Resource does not exist |
| 500 | Internal Error | Unhandled exception (log it, don't leak details) |
Close the server on SIGTERM so in-flight requests finish.
import { createServer } from "node:http";
const server = createServer((req, res) => {
res.writeHead(200, { "Content-Type": "text/plain" });
res.end("ok\n"
server.close() stops accepting new connections but lets active requests completeSIGTERM before removing pods from the servicePrevent slow clients from holding connections open indefinitely.
import { createServer } from "node:http";
const server = createServer((req, res) => {
res.writeHead(200, { "Content-Type": "text/plain" });
res.end("ok\n"
requestTimeout (Node 18+) destroys requests that exceed the limitheadersTimeout must be greater than requestTimeoutkeepAliveTimeout to match your reverse proxy - see Keep-Alive & Connection LimitsStream large files instead of reading them into memory.
import { createServer } from "node:http";
import { createReadStream } from "node:fs";
import { stat } from "node:fs/promises";
import { join } from "node:path";
const server = createServer(async (req, res
pipe handles backpressure automaticallyContent-Length when known for better client progress indicatorserror events on the read stream to avoid hung responsesnode:httpsWrap the same handler pattern in https.createServer with TLS certificates.
import { createServer as createHttpsServer } from "node:https";
import { readFileSync } from "node:fs";
const server = createHttpsServer(
{
key: readFileSync("certs/key.pem"),
cert: readFileSync("certs/cert.pem"),
mkcert or your platform's dev certificateshttp module in production?Rarely for application APIs. Use Express, Fastify, or NestJS for routing, parsing, and middleware. Know the raw module for debugging, health-check sidecars, and understanding what frameworks abstract away.
You likely forgot res.end() or called it twice. Each request needs exactly one terminal response. Check for code paths that fall through without ending.
async?Yes for I/O, but unhandled promise rejections in raw http handlers are not caught automatically. Wrap in try/catch or use a framework that handles async errors (Express 5 does).
writeHead and setHeader?writeHead sends status and headers immediately. setHeader queues headers until the first write or end. Mixing them after writes begin throws ERR_HTTP_HEADERS_SENT.
Set Access-Control-Allow-Origin and handle OPTIONS preflight manually, or use a framework middleware. See Security Middleware.
Stack versions: This page was written for Node.js 24.18.0 (Active LTS), npm 10+, TypeScript 5.6+, Express 5, Fastify 5, and NestJS 11.