This project is for educational purposes only. It is not intended for production use. It demonstrates how to build a stream-based, on-demand HTTP framework from scratch using Node.js internals.
A lightweight, stream-based HTTP framework for Node.js built from scratch in TypeScript.
Design principles:
- Stream based — built directly on Node.js
httpmodule, no abstraction layers - On demand — objects are created and recycled via pooling, not pre-allocated
- ESM — native ES modules throughout, no CommonJS
Every incoming request creates three objects: FastHttpContext, FastHttpRequest, and FastHttpResponse. Instead of allocating and discarding them per request, fasthttp reuses them through ObjectPool. When a response finishes, the pool calls a CLASS_CLEAR_SYMBOL method on each object to reset its state, then pushes it back for the next request. This reduces garbage collection pressure under high concurrency.
The router separates routes into two categories at registration time:
- Static routes — stored in a
Map<method, Map<path, handlers>>. Lookup is O(1). - Dynamic routes (with
:paramor*segments) — stored as arrays and scanned linearly per request.
At dispatch time, the RouterDispatcher first tries the static map. If no match, it falls through to dynamic routes and then to sub-routers. This gives fast static matching while still supporting flexible parameterized paths.
Handlers are executed sequentially via a runner function. Each handler receives (context, next). Calling next() advances to the next handler. If a handler throws or returns a rejected promise, the error is caught and emitted on the context's event emitter.
Routers can be nested using .router(subRouter). Each sub-router has its own base path and middleware stack. The dispatcher consumes path segments as it descends into sub-routers, so /users/42 dispatched through a /users sub-router will match /:id.
Expensive computations (URL parsing, header extraction, client IP detection) are wrapped in a Cache class that memoizes results per request. The cache is a simple Map<string, unknown> that gets cleared when the object is recycled.
Each context has an EventEmitter for lifecycle events: request, response, before-response, after-response, error, and router-level events (router:middlewares, router:handler, router:router). This lets you hook into the request lifecycle without modifying handlers.
npm install fasthttpimport { FastHttpApplication, FastHttpRouter } from "fasthttp";
const root = new FastHttpRouter().get((context) => {
context.response.text("Hello World");
});
new FastHttpApplication()
.createServer({ root })
.listen(3000, () => console.log("Listening on :3000"));import { FastHttpApplication, FastHttpRouter } from "fasthttp";
const items = [];
let nextId = 1;
const router = new FastHttpRouter()
.get("/items", (context) => {
context.response.json(items);
})
.get("/items/:id", (context) => {
const id = Number(context.request.parameters.id);
const item = items.find((i) => i.id === id);
if (!item) return context.response.status(404).text("Not found");
context.response.json(item);
})
.post("/items", async (context) => {
const body = await context.request.json();
const item = { id: nextId++, ...body };
items.push(item);
context.response.status(201).json(item);
})
.put("/items/:id", async (context) => {
const id = Number(context.request.parameters.id);
const index = items.findIndex((i) => i.id === id);
if (index === -1) return context.response.status(404).text("Not found");
const body = await context.request.json();
items[index] = { ...items[index], ...body };
context.response.json(items[index]);
})
.delete("/items/:id", (context) => {
const id = Number(context.request.parameters.id);
const index = items.findIndex((i) => i.id === id);
if (index === -1) return context.response.status(404).text("Not found");
items.splice(index, 1);
context.response.text("");
});
new FastHttpApplication()
.createServer({ root: router })
.listen(3000, () => console.log("Listening on :3000"));import { FastHttpApplication, FastHttpRouter } from "fasthttp";
const app = new FastHttpApplication({ timeoutMs: 5000 });
app.createServer({
root: new FastHttpRouter()
.use((context, next) => {
console.log(`${context.request.raw.method} ${context.request.raw.url}`);
next();
})
.get("/", (context) => {
context.response.text("Hello");
})
.get("/slow", async (context) => {
await new Promise((r) => setTimeout(r, 200));
context.response.text("Slow response");
}),
onRequest: (ctx) => {
ctx.event.once("error", (_err, _req, res) => {
res.status(500).text("Server error");
});
},
}).listen(3000);import { FastHttpApplication, FastHttpRouter } from "fasthttp";
const users = new FastHttpRouter("/users")
.get("/:id", (context) => {
context.response.json({ userId: Number(context.request.parameters.id) });
});
const posts = new FastHttpRouter("/posts")
.get("/:id", (context) => {
context.response.json({ postId: Number(context.request.parameters.id) });
});
new FastHttpApplication()
.createServer({
root: new FastHttpRouter()
.router(users)
.router(posts),
})
.listen(3000, () => console.log("Listening on :3000"));GET /users/42 returns { "userId": 42 }.
GET /posts/7 returns { "postId": 7 }.
import { FastHttpApplication, FastHttpRouter } from "fasthttp";
new FastHttpApplication()
.createServer({
root: new FastHttpRouter()
.post("/echo", async (context) => {
context.response.json(await context.request.json());
})
.post("/uppercase", async (context) => {
const text = await context.request.text();
context.response.text(text.toUpperCase());
}),
})
.listen(3000);import { FastHttpApplication, FastHttpRouter } from "fasthttp";
const root = new FastHttpRouter().get((context) => {
context.response.text("Hello World");
});
const application = new FastHttpApplication();
const server = application.createServer({
root,
onRequest: (context) => {
const start = performance.now();
context.event.once("response", () => {
const end = performance.now();
console.log(`Request ${context.request.id}: ${(end - start).toFixed(2)}ms`);
});
},
});
server.listen(3000);| Option | Type | Default | Description |
|---|---|---|---|
timeoutMs |
number |
— | Request timeout passed to http.createServer |
timerWheelResolution |
number |
100 |
Timer wheel tick interval in ms |
pool.maxObjectSize |
number |
100 |
Max pooled objects per pool |
const app = new FastHttpApplication({ timeoutMs: 30000 });
const server = app.createServer({ root, onRequest });| Method | Description |
|---|---|
.get(path, ...handlers) |
Register GET handler |
.post(path, ...handlers) |
Register POST handler |
.put(path, ...handlers) |
Register PUT handler |
.patch(path, ...handlers) |
Register PATCH handler |
.delete(path, ...handlers) |
Register DELETE handler |
.use(...handlers) |
Register middleware |
.router(subRouter) |
Mount a sub-router |
Routes support static paths (/items) and dynamic segments (/items/:id).
Access via context.request (or context.req).
| Property/Method | Description |
|---|---|
context.request.raw |
Raw Node.js IncomingMessage |
context.request.parameters |
Route params (:id -> { id: "42" }) |
context.request.headers |
Request headers |
context.request.id |
Request ID (from x-request-id header or auto-generated UUID) |
context.request.ip |
Client IP address |
context.request.url |
Parsed URL object |
context.request.text() |
Read body as string |
context.request.json() |
Read body as parsed JSON |
context.request.buffer() |
Read body as Buffer |
context.request.stream() |
Raw readable stream |
context.request.extras |
Arbitrary key-value store per request |
Access via context.response (or context.res).
| Method | Description |
|---|---|
context.response.status(code) |
Set status code (chainable) |
context.response.text(payload, contentType?) |
Send plain text (text/plain) |
context.response.json(payload) |
Send JSON (application/json) |
context.response.buffer(payload, contentType?) |
Send buffer (application/octet-stream) |
context.response.stream(contentType?) |
Get raw writable stream |
context.response.locals |
Arbitrary key-value store per response |
context.response.extras |
Arbitrary key-value store per response |
| Property/Method | Description |
|---|---|
ctx.request |
FastHttpRequest |
ctx.response |
FastHttpResponse |
ctx.method |
Lowercase HTTP method |
ctx.url |
Parsed URL |
ctx.paths |
URL path segments |
ctx.event |
EventEmitter for lifecycle events |
ctx.abortController |
AbortController for this request |
ctx.running |
Whether a handler has been dispatched |
| Event | Arguments | Description |
|---|---|---|
request |
(request, response) |
Fired when request starts |
response |
(request, response) |
Fired after response finishes |
before-response |
(payload) |
Fired before response is sent |
after-response |
— | Fired after response is sent |
error |
(error, request, response) |
Fired on unhandled errors |
FastHttpRequestError— thrown by request header parsing methodsFastHttpResponseError— response-level errors
Results from autocannon (100 concurrent connections, 10 seconds per scenario) on 2026-07-09.
| Scenario | fasthttp | express | fastify |
|---|---|---|---|
| JSON | 13,250 req/s | 15,998 req/s | 26,301 req/s |
| Plaintext | 14,542 req/s | 16,298 req/s | 26,218 req/s |
| Params | 17,480 req/s | 14,441 req/s | 25,734 req/s |
| Echo (small) | 13,578 req/s | 10,917 req/s | 15,706 req/s |
| Echo (large) | 11,055 req/s | 4,381 req/s | 4,536 req/s |
| Scenario | fasthttp | express | fastify |
|---|---|---|---|
| JSON | 18ms | 10ms | 5ms |
| Plaintext | 17ms | 9ms | 5ms |
| Params | 15ms | 9ms | 4ms |
| Echo (small) | 18ms | 12ms | 14ms |
| Echo (large) | 16ms | 32ms | 31ms |
Key takeaways:
- Fasthttp outperforms Express on parameterized routes (+21%) and large echo payloads (+152%)
- Fasthttp has lower p99 latency than Express on echo-large (16ms vs 32ms)
- Fastify leads across the board due to its optimized serialization layer and schema-based routing
- These benchmarks are educational — the frameworks have different feature sets and optimization targets
npm run bench # standard benchmark
npm run bench:stress # stress testfasthttp/
├── src/
│ ├── application.ts # FastHttpApplication — server creation, pool management
│ ├── router.ts # FastHttpRouter — route registration, static/dynamic storage
│ ├── router-dispatcher.ts # RouterDispatcher — route matching and handler execution
│ ├── context.ts # FastHttpContext — per-request context, events, abort control
│ ├── request.ts # FastHttpRequest — request wrapper, header/body parsing
│ ├── response.ts # FastHttpResponse — response wrapper, text/json/buffer/stream
│ ├── cache.ts # Cache — lazy memoization for computed values
│ ├── errors.ts # FastHttpRequestError, FastHttpResponseError
│ ├── constant.ts # Shared symbols and constants
│ ├── types.ts # TypeScript type definitions
│ └── pools/
│ ├── objects.ts # ObjectPool — generic object recycling
│ └── timer-wheel.ts # TimerWheel — efficient timeout management
├── example/
│ ├── simple.js # Minimal hello world
│ ├── routing.js # CRUD routes with parameters
│ ├── middleware.js # Middleware and error handling
│ ├── sub-router.js # Sub-router composition
│ ├── json-body.js # JSON/text body handling
│ └── performance.js # Request timing
├── benchmark/
│ ├── run.js # Benchmark runner (fasthttp vs express vs fastify)
│ ├── stress.js # Stress test
│ ├── servers/ # Benchmark server implementations
│ └── history/ # Saved benchmark results
└── test/
├── e2e/ # End-to-end tests
└── helpers/ # Test utilities
ISC