superexpressjs
v1.1.0
Published
A minimal Express-style web framework built from scratch on raw TCP sockets — routing, middlewares, error handling and mountable routers with zero dependencies
Maintainers
Readme
SuperExpress.js
A minimal Express-style web framework for Node.js, built from scratch on raw TCP sockets (node:net) — no http module, zero dependencies. Built as a learning project to understand what frameworks like Express actually do under the hood: HTTP parsing, routing, middleware chains, and error handling.
import { superExpress } from "superexpressjs";
const app = superExpress();
app.get("/users/:id", (req, res) => {
res.json({ id: req.params.id });
});
app.listen(3000);Install
npm install superexpressjsRequires Node.js >= 18. Ships as ESM with full TypeScript types.
Table of Contents
Features
- HTTP from scratch — raw request parsing and response writing over
node:netTCP sockets; thehttpmodule is never imported - Routing —
get,post,put,patch,deletewith route params (/users/:id) and query string parsing (?page=1&limit=10) - Three middleware levels — app-level (
app.use), router-level (router.use), and route-level (app.get(path, mw1, mw2, handler)) - Async-safe error handling — errors thrown from handlers and middlewares, sync or async, are caught — no unhandled promise rejections
- Error middlewares — Express-style
(error, req, res, next)chain viaapp.useError, with a default 500 fallback - Mountable routers — group related routes in a
Router()and mount them under a path prefix - Built-in middleware factories —
cors,json,logger,rateLimit,serveStatic(with directory-traversal protection) - TypeScript-first — strict types for
Request,Response,Handler,Middleware,ErrorMiddleware - Zero runtime dependencies
Architecture
┌────────────────────────────────────────────┐
│ superExpress() │
│ │
TCP connection │ ┌──────────────┐ ┌──────────────────┐ │
──────────────────────▶ │ │ net.Server │───▶│ requestParser │ │
raw bytes │ │ (socket) │ │ raw text → req │ │
│ └──────────────┘ └────────┬─────────┘ │
│ │ │
│ ▼ │
│ ┌──────────────────────────────────────┐ │
│ │ app-level middlewares │ │
│ │ cors → rateLimit → serveStatic … │ │
│ └────────────────┬─────────────────────┘ │
│ │ next() │
│ ▼ │
│ ┌──────────────┐ ┌──────────────────┐ │
│ │ findRoute │───▶│ route middlewares│ │
│ │ exact → :param│ │ then handler │ │
│ └──────────────┘ └────────┬─────────┘ │
│ │ │
│ any error, sync or async │
│ ▼ │
│ ┌──────────────────────────────────────┐ │
│ │ error middlewares │ │
│ │ (error, req, res, next) → … → 500 │ │
│ └──────────────────────────────────────┘ │
└────────────────────────────────────────────┘Source layout:
src/
├── index.ts # public API barrel (superExpress, Router, middlewares, types)
├── server.ts # superExpress(): TCP server, dispatch loop, app.use overloads
├── types.ts # Request, Response, Handler, Middleware, ErrorMiddleware, Route
├── parser/
│ └── requestParser.ts # raw HTTP text → Request (method, path, headers, query, body)
├── router/
│ ├── router.ts # Router(): mountable route group factory
│ ├── findRoute.ts # route table lookup (exact match first, then :param routes)
│ └── matchRoute.ts # segment-by-segment :param matching → params object
├── middleware/
│ ├── middlewareRegistery.ts # app-level middleware + error-middleware storage
│ └── factory/ # built-ins: cors, json, logger, rateLimit, staticServer
├── constants/
│ └── http.ts # status code → reason phrase map
└── utils/
├── handleError.ts # default error handler (log + 500)
├── joinPath.ts # mount-prefix + route-path joining
└── getMimeType.ts # file extension → Content-TypeHow It Works
1. The TCP server
app.listen(port) creates a net.Server. Each connection's data event delivers the raw HTTP request as plain text — there is no http.IncomingMessage; everything above the socket is implemented in this library:
GET /users/42?verbose=1 HTTP/1.1\r\n
host: localhost:3000\r\n
\r\n2. Request parsing
parseReq (src/parser/requestParser.ts) turns that text into a Request object:
- Split head from body at the first
\r\n\r\n - Parse the request line →
method,path,version - Split
pathon?→ path name + query string; decodekey=value&…pairs intoreq.query - Parse each header line on the first
:→ lowercase keys inreq.headers
A malformed request short-circuits to 400 Bad Request. req.params starts empty and is filled in by the router.
3. The response object
A fresh res is built per request, writing HTTP directly to the socket:
res.status(code)/res.setHeader(key, value)— chainable settersres.send(body)— writes the status line (HTTP/1.1 200 OK), headers, blank line, and body, then closes the socketres.json(data)— setsContent-Type: application/jsonand sendsJSON.stringify(data)
4. Routing
Every route registration (app.get(path, ...handlers)) pushes a Route record — the last handler is the route handler, anything before it becomes route-level middleware:
{ method: "GET", path: "/users/:id", middlewares: [...], handler }Lookup (findRoute) is two-phase:
- Exact match on method + path — fast path
- Param match — for routes containing
:,matchRoutecompares segment-by-segment; literal segments must be equal,:paramsegments capture intoreq.params
/users/42 against /users/:id → { id: "42" }. No match → 404.
5. The middleware chain
Dispatch is a pair of recursive next() chains:
runMiddleware(0) // app-level: cors → rateLimit → static → …
└─ next() → runMiddleware(1) → … (exhausted)
└─ runRouteMiddleware(0) // router-level + route-level middlewares
└─ next() → … (exhausted)
└─ route handlerEach middleware decides whether to call next(). Not calling it ends the chain — e.g. rateLimit responds 429 directly, serveStatic sends the file and never calls next().
6. Async-safe error handling
The classic Express 4 footgun: an async handler that throws doesn't throw into the framework — the call returns instantly with a rejected promise, the stack is gone, and a plain try/catch sees nothing. The result is an unhandled rejection and a connection that hangs.
SuperExpress handles both failure modes at every call site (handlers, middlewares, error middlewares):
try {
const result = handler(req, res, next);
Promise.resolve(result).catch((error) => runErrorMiddleware(error, 0));
} catch (error) {
runErrorMiddleware(error, 0); // sync throws
}Promise.resolve(result) is the trick: a sync handler returns undefined (resolves immediately, .catch never fires); an async handler returns a promise whose rejection is now observed. The framework never needs to await anything — handlers drive the response themselves via res.send(); the return value is only watched for failure. This is essentially what Express 5 added.
Caught errors run the error middleware chain (app.useError): each error middleware can respond or call next() to pass the error on. If the chain is exhausted (or an error middleware itself throws), the default handler logs and sends 500 Internal Server Error.
7. Routers and mounting
Router() is a self-contained route group: its own routes array, verbs, and router-level middlewares. Mounting flattens it into the app's route table:
app.use("/users", userRouter);
// for each router route:
// path → joinPath("/users", route.path) "/" → "/users", "/:id" → "/users/:id"
// middlewares → [...router.middlewares, ...route.middlewares]Because mounted routes land in the same route table, they flow through the exact same dispatch and error handling as app routes — router support required zero changes to the dispatch loop. The trade-off: mounting copies a snapshot, so register routes on the router before app.use(prefix, router).
Examples
Hello world
import { superExpress } from "superexpressjs";
const app = superExpress();
app.get("/", (req, res) => {
res.send("<h1>Hello from raw TCP!</h1>");
});
app.listen(3000);Route params and query strings
// GET /users/42?verbose=1
app.get("/users/:id", (req, res) => {
res.json({
id: req.params.id, // "42"
verbose: req.query.verbose, // "1"
});
});JSON body parsing
import { json } from "superexpressjs";
app.post("/users", json(), (req, res) => {
// req.body is parsed when Content-Type: application/json
res.status(201).json({ created: req.body });
});App-level middlewares
import { cors, logger, rateLimit, serveStatic } from "superexpressjs";
app.use(logger()); // method + path per request
app.use(cors({ origins: ["http://localhost:5173"] }));
app.use(rateLimit({ limit: 200, windowMs: 60_000 })); // 429 over the limit
app.use(serveStatic("./public")); // files, with traversal protectionRoute-level middlewares
const requireAuth = (req, res, next) => {
if (req.headers["authorization"] !== "secret") {
return res.status(401).send("Unauthorized");
}
next();
};
app.get("/admin", requireAuth, (req, res) => {
res.send("admin area");
});Routers
import { superExpress, Router, logger } from "superexpressjs";
const app = superExpress();
const userRouter = Router();
userRouter.use(logger()); // scoped to this router only
userRouter.get("/", (req, res) => res.send("all users"));
userRouter.get("/:id", (req, res) => res.json({ id: req.params.id }));
userRouter.post("/", (req, res) => res.status(201).send("created"));
app.use("/users", userRouter);
// GET /users · GET /users/:id · POST /usersError handling
// throw from anywhere — sync or async — and it's caught
app.get("/boom", async () => {
throw new Error("Async Boom"); // no unhandled rejection
});
// error middlewares run in registration order
app.useError((error, req, res, next) => {
if (error instanceof Error && error.message === "teapot") {
return res.status(418).send("I'm a teapot");
}
next(); // not mine — pass it on
});
app.useError((error, req, res, next) => {
res.status(500).json({
error: error instanceof Error ? error.message : "Internal Server Error",
});
});If no error middleware responds, the default handler logs the error and sends 500.
Full application
See examples/basic.ts for a complete app combining CORS, rate limiting, static files, a mounted router, and error middlewares. Run it from a clone of this repo with:
npm run devAPI Reference
superExpress()
Creates an app.
| Method | Description |
|---|---|
| app.get/post/put/patch/delete(path, ...handlers) | Register a route; last handler is the route handler, the rest are route-level middlewares |
| app.use(middleware) | Register an app-level middleware |
| app.use(prefix, router) | Mount a Router() under a path prefix |
| app.useError(errorMiddleware) | Register an (error, req, res, next) error middleware |
| app.listen(port) | Start the TCP server |
Router()
Creates a mountable route group with the same verb methods plus router.use(middleware) for router-level middlewares.
Request
| Property | Type | Description |
|---|---|---|
| method | string | HTTP method |
| path | string | Path without query string |
| params | Record<string, string> | Route params from :segments |
| query | Record<string, string> | Decoded query string pairs |
| headers | Record<string, string> | Lowercased header names |
| body | string | Raw body (object after json() middleware) |
Response
| Method | Description |
|---|---|
| res.status(code) | Set status code (chainable) |
| res.setHeader(key, value) | Set a header (chainable) |
| res.send(body) | Write the response and close the connection |
| res.json(data) | Send JSON with the right Content-Type |
Built-in middleware factories
| Factory | Description |
|---|---|
| cors({ origins }) | Sets Access-Control-Allow-Origin for allowed origins |
| json() | Parses application/json bodies into req.body; 400 on invalid JSON |
| logger() | Logs timestamp -> [METHOD] /path per request |
| rateLimit({ limit, windowMs }) | Fixed-window rate limiting; 429 over the limit |
| serveStatic(rootDir) | Serves files with MIME detection (.html, .css, .js, .json, .png, .jpg) and path-traversal protection |
Known Limitations
This is a learning project, not a production framework:
- Assumes one complete HTTP request per TCP
dataevent — noContent-Lengthbuffering or chunked transfer encoding, so large request bodies may be truncated - No keep-alive or pipelining — every response closes the connection
- Routes must be registered on a router before mounting it (mounting copies a snapshot)
- Rate limiting is in-memory, per-process, and global (not per-IP)
License
ISC
