HTTP Basics in Node
10 examples to understand HTTP in Node.js with http.createServer - 7 basic and 3 intermediate.
Prerequisites
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.
Basic Examples
1. Minimal HTTP Server
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");
});
server.listen(3000, () => {
console.log("Listening on http://localhost:3000");
});reqis anIncomingMessagewithmethod,url, andheadersresis aServerResponse- you must callres.end()or the client waits foreverwriteHeadsets status and headers in one call; use it before the firstwrite
Related: Routing Without Frameworks - match paths without Express
2. Reading the Request Method and URL
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 ?? "/", true);
if (req.method === "GET" && pathname === "/health") {
res.writeHead(200, { "Content-Type": "application/json" });
res.end(JSON.stringify({ status: "ok" }));
return;
}
res.writeHead(404, { "Content-Type": "application/json" });
res.end(JSON.stringify({ error: "Not found" }));
});req.urlincludes query string - useURLorparsefromnode:urlfor pathname- Return early after sending a response to avoid double-writes
- Frameworks like Express hide this parsing behind
req.pathandreq.query
3. Reading Request Headers
Headers 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"] ?? "unknown";
res.writeHead(200, { "Content-Type": "application/json" });
res.end(JSON.stringify({ contentType, userAgent }));
});- Header names are lowercased automatically
- Duplicate headers are joined with
,in a single string value - Never trust
X-Forwarded-Forwithout proxy configuration - see Reverse Proxy Awareness
4. Sending JSON Responses
Set 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": "application/json; charset=utf-8",
});
res.end(JSON.stringify({ data: users }));
});- Always include
charset=utf-8for JSON with non-ASCII text - Use
JSON.stringifywith a replacer for dates:date.toISOString() - Framework serializers (Fastify) compile JSON schemas for speed
5. Reading a JSON Request Body
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 (const chunk of req) {
chunks.push(chunk as Buffer);
}
return Buffer.concat(chunks).toString("utf8");
}
const server = createServer(async (req, res) => {
if (req.method !== "POST") {
res.writeHead(405).end();
return;
}
try {
const body = JSON.parse(await readBody(req));
res.writeHead(201, { "Content-Type": "application/json" });
res.end(JSON.stringify({ received: body }));
} catch {
res.writeHead(400, { "Content-Type": "application/json" });
res.end(JSON.stringify({ error: "Invalid JSON" }));
}
});- Request bodies are streams - read them asynchronously
- Set a size limit before concatenating chunks (production requirement)
- Express
express.json()and Fastify's built-in parser handle this with limits
6. Setting Response Status Codes
Use 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": "application/json" });
res.end(JSON.stringify({ error: "User not found" }));
return;
}
res.writeHead(200, { "Content-Type": "application/json" });
res.end(JSON.stringify({ id: 1, name: "Ada" }));
});| 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) |
7. Graceful Server Shutdown
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.listen(3000);
function shutdown() {
console.log("Shutting down...");
server.close(() => {
console.log("Server closed");
process.exit(0);
});
setTimeout(() => process.exit(1), 10_000).unref();
}
process.on("SIGTERM", shutdown);
process.on("SIGINT", shutdown);server.close()stops accepting new connections but lets active requests complete- Set a force-exit timeout so a stuck handler does not block deploys forever
- Kubernetes sends
SIGTERMbefore removing pods from the service
Intermediate Examples
8. Server Timeouts
Prevent 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");
});
server.requestTimeout = 30_000; // 30s per request
server.headersTimeout = 35_000; // must exceed requestTimeout
server.keepAliveTimeout = 5_000; // idle keep-alive socket timeout
server.listen(3000);requestTimeout(Node 18+) destroys requests that exceed the limitheadersTimeoutmust be greater thanrequestTimeout- Tune
keepAliveTimeoutto match your reverse proxy - see Keep-Alive & Connection Limits
9. Streaming a File Response
Stream 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) => {
const filePath = join(process.cwd(), "public", "report.pdf");
try {
const info = await stat(filePath);
res.writeHead(200, {
"Content-Type": "application/pdf",
"Content-Length": info.size,
});
createReadStream(filePath).pipe(res);
} catch {
res.writeHead(404).end();
}
});pipehandles backpressure automatically- Set
Content-Lengthwhen known for better client progress indicators - Handle
errorevents on the read stream to avoid hung responses
10. HTTPS with node:https
Wrap 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"),
},
(req, res) => {
res.writeHead(200, { "Content-Type": "text/plain" });
res.end("HTTPS ok\n");
}
);
server.listen(3443);- In production, terminate TLS at nginx, ALB, or Cloudflare - Node handles HTTP behind the proxy
- For local dev, use
mkcertor your platform's dev certificates - HTTP/2 and HTTP/3 are typically handled at the proxy layer - see HTTP/2 & HTTP/3 Considerations
FAQs
Do I need to use the raw http 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.
Why does my server hang after sending a response?
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.
Should handlers be 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).
What is the difference between 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.
How do I handle CORS in raw HTTP?
Set Access-Control-Allow-Origin and handle OPTIONS preflight manually, or use a framework middleware. See Security Middleware.
Related
- Routing Without Frameworks - URL matching without Express
- Middleware Pattern - composable request pipelines
- Keep-Alive & Connection Limits - connection tuning
- Express Basics - the most common framework layer
- HTTP Fundamentals Best Practices - section checklist
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.