A production-grade HTTP/1.1 server framework written from scratch on raw TCP sockets, in TypeScript — with zero runtime dependencies.
The entire HTTP protocol layer is hand-implemented: request parsing happens byte-by-byte on top of Node's net sockets, not the http module. Routing, keep-alive, chunked transfer-encoding, WebSockets, and the full middleware stack are all built from the ground up. The only Node built-ins used are the unavoidable ones — net/tls (transport), crypto (hashing/JWT/WebSocket keys), and zlib (compression).
Think Express, but you can open the hood all the way down to the wire.
import { createServer, json, requestLogger } from "forge-http";
const app = createServer();
app.use(requestLogger());
app.use(json());
app.get("/users/:id", (req, res) => {
res.json({ id: req.params.id });
});
await app.listen(3000);Most "build an HTTP server" projects either (a) wrap the http module and add routes, or (b) parse one fixed request and exit. This one implements the actual protocol, handles the hard parts correctly, and packages it as a real, usable framework with the production middleware you'd expect.
Highlights of what's implemented from first principles:
- A streaming HTTP/1.1 parser that consumes arbitrary TCP chunks, reassembles requests split across packets, and emits multiple pipelined requests from a single buffer.
- Connection lifecycle management — persistent keep-alive connections, request pipelining (in-order responses), idle + slowloris timeouts, and a per-connection request ceiling.
- Security-correct framing — rejects HTTP request smuggling (
Transfer-Encoding+Content-Length), conflictingContent-Lengthheaders, obsolete line folding, and oversized headers/bodies. - WebSockets (RFC 6455) — the opening handshake plus a complete frame codec: fragmentation, client-frame unmasking, and ping/pong/close control frames.
- JWT (HS256/384/512) — sign and verify with constant-time comparison and algorithm pinning to defeat
alg:noneconfusion attacks. - A radix-trie router — O(path-depth) route matching with params, wildcards, and nested sub-routers.
| Area | What's included |
|---|---|
| Protocol core | Byte-level HTTP/1.1 parser · keep-alive · pipelining · chunked transfer-encoding (in & out) · HTTP/1.0 fallback · TLS/HTTPS |
| Routing | Radix-trie router · path params (:id) · wildcards (*path) · nested routers · 405 with Allow · method dispatch |
| Middleware | Async onion-model engine · global + route-scoped · centralized error handling |
| Body parsing | JSON · urlencoded · text · raw · multipart/form-data file uploads (from-scratch multipart parser) |
| Static files | MIME detection · Range requests / 206 (video streaming) · ETag · Last-Modified · conditional 304 · streaming large files |
| Compression | gzip · brotli · deflate, with Accept-Encoding negotiation (buffered + streamed paths) |
| Real-time | WebSockets (RFC 6455) · Server-Sent Events (SSE) · pub/sub broadcast hub |
| Auth | JWT (HS256/384/512) · HTTP Basic · API key · role guards · constant-time secret comparison |
| Security | CORS (with preflight) · helmet-style headers (CSP/HSTS/etc.) · signed cookies · sessions |
| Rate limiting | Sliding-window counter · token bucket · per-key with idle eviction |
| Validation | A small zod-like schema library with coercion + path-aware error reporting |
| Observability | Structured logging w/ request IDs · Prometheus /metrics (counter/gauge/histogram) · liveness + readiness health checks |
| Reliability | Graceful shutdown (drain in-flight) · keep-alive/header timeouts · request-size limits · LRU cache |
TCP / TLS socket (node:net / node:tls)
│ raw bytes
▼
┌─────────────────────────────────────────────────────────────────┐
│ Connection keep-alive · pipelining · timeouts │
│ │ socket hijack (WebSocket upgrade) │
│ ▼ │
│ RequestParser incremental HTTP/1.1 state machine │
│ │ Content-Length · chunked · smuggling guard │
│ ▼ │
│ Request / Response URL+query parse · content negotiation · │
│ │ buffered + chunked-streaming serializer │
│ ▼ │
│ Application ┌─ global middleware (onion compose) ──┐ │
│ │ │ logger · cors · auth · rate-limit … │ │
│ │ └──────────────┬───────────────────────┘ │
│ ▼ ▼ │
│ Router (radix trie) match method + path → handler chain │
│ │ │
│ ▼ error handler · 404 · 405 │
└─────────────────────────────────────────────────────────────────┘
src/
├── core/ the protocol layer
│ ├── parser.ts ← byte-level HTTP/1.1 parser (the heart)
│ ├── connection.ts ← per-socket state machine, keep-alive, pipelining
│ ├── server.ts ← TCP/TLS listener + graceful shutdown
│ ├── request.ts ← Request: URL/query parse, content negotiation
│ ├── response.ts ← Response: buffered + chunked-streaming serializer
│ ├── headers.ts ← case-insensitive multi-value header bag
│ ├── http-error.ts ← status-carrying error type
│ ├── status.ts ← status codes + reason phrases
│ └── mime.ts ← MIME table + compressibility
├── router/
│ ├── trie.ts ← radix tree for route matching
│ ├── router.ts ← Express-style router + mounting
│ └── compose.ts ← async onion-model middleware engine
├── middleware/ logger · body-parser · static · compression · cors ·
│ security-headers · rate-limit · cookie · session ·
│ auth · validate · error-handler
├── features/ websocket · sse · jwt · validation · metrics · health
├── utils/ logger · lru
├── app.ts the Application (router + server + WS upgrade routing)
└── index.ts public API
npm install # only dev deps: typescript + @types/node
npm run build # compile to dist/
npm test # run the unit + integration suiteRun the examples:
npm run example:rest # REST API with JWT auth, validation, rate limiting → :3000
npm run example:chat # WebSocket chat room (open the printed URL) → :3001
npm run example:files # static server w/ compression + range requests → :3002
npm run start # the "kitchen sink" showcase → :3000
npm run bench # throughput benchmarkimport { createServer, Router } from "forge-http";
const app = createServer();
const api = new Router();
api.get("/users/:id", (req, res) => res.json({ id: req.params.id }));
api.get("/files/*path", (req, res) => res.json({ path: req.params.path })); // wildcard
app.use("/api/v1", api); // mounted under a prefix; req.baseUrl is set
await app.listen(3000);app.use(async (req, res, next) => {
const start = Date.now();
await next(); // descend into inner layers
req.log.info(`took ${Date.now() - start}ms`);
});import { json, multipart } from "forge-http";
app.post("/json", json(), (req, res) => res.json(req.body));
app.post("/upload", multipart(), (req, res) => {
const file = req.files["avatar"]; // { filename, contentType, size, data }
res.json({ uploaded: file });
});import { validate, v } from "forge-http";
const schema = v.object({
email: v.string().email(),
age: v.number().int().min(0),
role: v.enum("user", "admin").default("user"),
});
app.post("/signup", validate({ body: schema }), (req, res) => {
res.status(201).json(req.body); // body is coerced + validated
});import { signJwt, bearerAuth, requireAuth, requireRole } from "forge-http";
app.post("/login", (req, res) => {
const token = signJwt({ sub: "user-1", roles: ["admin"] }, SECRET, { expiresIn: 3600 });
res.json({ token });
});
app.use("/admin", bearerAuth({ secret: SECRET }), requireAuth(), requireRole("admin"));import { WebSocketHub } from "forge-http";
const hub = new WebSocketHub();
app.ws("/ws", (ws, req) => {
hub.add(ws);
ws.on("message", (data) => hub.broadcast(data)); // echo to all peers
ws.on("close", () => console.log("bye"));
});import { createSSE } from "forge-http";
app.get("/events", (req, res) => {
const sse = createSSE(res, { heartbeatMs: 15_000 });
const t = setInterval(() => sse.send({ time: Date.now() }), 1000);
sse.onClose(() => clearInterval(t));
});import { HttpMetrics, HealthRegistry } from "forge-http";
const metrics = new HttpMetrics();
app.use(metrics.middleware());
app.get("/metrics", metrics.handler()); // Prometheus exposition
const health = new HealthRegistry().register("db", () => checkDb());
app.get("/healthz", health.liveness());
app.get("/readyz", health.readiness());import { readFileSync } from "node:fs";
const app = createServer({
tls: { cert: readFileSync("cert.pem"), key: readFileSync("key.pem") },
});app.enableGracefulShutdown(); // SIGINT/SIGTERM → stop accepting, drain, exitThe parser is deliberately strict, because lenient HTTP parsers are where smuggling bugs live:
- Request smuggling —
Transfer-Encodingtogether withContent-Lengthis rejected (400); the only supported terminal transfer-coding ischunked. - Conflicting
Content-Lengthvalues are rejected. - Obsolete line folding (RFC 7230 §3.2.4) is rejected.
- Resource limits — header block (
431) and body (413) sizes are bounded; slow header delivery trips a slowloris timeout (408). - Constant-time comparisons for cookie signatures, JWT signatures, API keys, and Basic credentials.
- JWT algorithm pinning rejects
alg:noneand unexpected algorithms. - Path-traversal protection in the static file server (resolved paths must stay within the root).
41 tests across unit and end-to-end suites:
parser.test.ts— request parsing, chunked decoding, pipelining, split packets, smuggling/limit rejections.router.test.ts— static/param/wildcard matching, priority,405detection.jwt.test.ts— sign/verify round-trip, tampering, expiry,alg:none, issuer/audience.validation.test.ts— coercion, nested schemas, error paths.integration.test.ts— boots a real server and drives it over HTTP: JSON, params, query arrays,404/405, compression, CORS preflight, rate limiting,HEAD, keep-alive reuse.
npm test
# ℹ tests 41 ℹ pass 41 ℹ fail 0A quick local run (npm run bench, 20k requests @ 50 concurrent, keep-alive) on a developer laptop:
throughput ~10,000 req/s
latency p50 ~4.5 ms
latency p90 ~5.7 ms
latency p99 ~8.0 ms
Not tuned for raw speed (the focus is correctness and feature completeness), but comfortably in a reasonable range for a hand-rolled stack.
- Node.js ≥ 20 (developed on Node 24)
- TypeScript 5+ (dev dependency only)
MIT