votrix
v1.0.1
Published
A lightweight reusable HTTP framework for Node.js with middleware, routing and async logging.
Readme
Votrix
1. Architecture
App wraps node:http.createServer, installs optional middleware, and forwards (req, res) to Router.handleRequest().
Router owns:
- request decoration
- middleware execution
- route matching
- query parsing
- handler dispatch
- error propagation
- fallback
404and500responses
sendResponse is the terminal serializer. It handles undefined, Uint8Array, string, and JSON-serializable values.
createParseBodyMiddleware and createRequestLoggerMiddleware are optional runtime components. They are installed by App and stay outside the router core.
Route storage is split into two structures:
- static routes:
Record<HttpMethod, Map<string, RegisteredRoute>> - dynamic routes: a tree of
RouteNodeobjects withchildren,dynamicChild, and per-method terminal handlers
Exact routes such as GET /health resolve through a single Map.get(pathname) call. Dynamic routes such as GET /users/:id traverse the route tree segment by segment.
Middleware flow is explicit:
- standard middleware:
(req, res, next) - error middleware:
(req, res, next, error)
Error middleware is selected by function arity. Handlers that do not use next run without delegated middleware flow.
graph TD
A[Incoming Request] --> B[App Server Callback]
B --> C[Router.handleRequest]
C --> D[Middleware Execution]
D --> E[Route Match]
E --> F[Handler Execution]
F --> G[sendResponse]
G --> H[Socket Write]2. Request Pipeline
createServerreceives(req, res).App.listen()forwards the pair intorouter.handleRequest(req, res).Router.decorateRequest()casts the Node request intoFrameworkRequestand initializesreq.params = {}andreq.query = {}.Router.handle()readsreq.url, computes the query boundary, and chooses between direct route dispatch and middleware execution.- If middleware is present,
Router.runMiddlewares()executes the chain in registration order. createParseBodyMiddleware()skipsGETandDELETE, skips requests without body headers, parses JSON when a body exists, and forwards invalid JSON as400 Invalid JSON.createRequestLoggerMiddleware()records timestamps and logs completion or early socket close without changing routing behavior.Router.dispatchRoute()normalizes the pathname and callsmatchRoute(req.method, pathname).matchRoute()checks the method-specific static map first.- If no static route matches,
matchRoute()walks theRouteNodetree, preferring static children and falling back todynamicChild. - Query parsing runs only after a successful route match and only when
?exists in the raw URL. - Route params are materialized only for dynamic matches.
- The resolved handler executes.
- If the handler returns a value and the response is still open,
sendResponse()serializes the result. - If the handler throws or calls
next(error),runErrorMiddlewares()executes error middleware or falls back to500 Internal Server Error. - If no route matches, the router returns
404 Not Found.
Request Flow
graph TD
A[Incoming Request] --> B[Decorate Request]
B --> C[Run Middleware]
C --> D[Normalize Path]
D --> E[Static Route Lookup]
E --> F{Matched?}
F -- Yes --> G[Optional Query Parse]
F -- No --> H[Dynamic Tree Walk]
H --> I{Matched?}
I -- No --> J[404]
I -- Yes --> G
G --> K[Handler Execution]
K --> L[sendResponse]
L --> M[res.end]3. Hot Path
Reference route:
- method:
GET - path:
/health - response:
{ "status": "ok" }
Execution path:
- Node invokes the server callback.
Appforwards the request toRouter.handleRequest().decorateRequest()allocates emptyparamsandqueryobjects.- Body parser middleware exits immediately because the method is
GET. dispatchRoute()computes the pathname and detects that no query string is present.matchRoute()resolves the handler fromstaticRoutes.GET.get("/health").- No dynamic tree traversal occurs.
- No query parsing occurs.
- No param materialization occurs.
- The handler returns a small object synchronously.
sendResponse()setsContent-Type: application/json; charset=utf-8.JSON.stringify()serializes the object.res.end()writes the payload.
Computational cost by stage:
- request decoration: two object assignments
- body parsing: one method check and early return
- routing: one exact
Maplookup - handler dispatch: direct function call
- serialization: one
JSON.stringify()call - response write: one
res.end()
No route scan occurs. No query parser runs. No dynamic param extraction runs. No mandatory promise chain runs for a synchronous handler.
Hot Path Diagram
graph LR
Req[GET /health] --> Router[Static Map Lookup]
Router --> Handler[Sync Handler]
Handler --> Res[sendResponse + res.end]4. Performance
Performance comes from reducing work on the request path.
Static route fast path
Static routes bypass dynamic traversal. matchRoute() reads staticRoutes[method].get(pathname) before touching the route tree. Simple endpoints pay for one hash-map lookup instead of per-route iteration or segment capture logic.
Deferred parsing
Parsing is conditional:
- query strings are parsed only after a successful route match
- route params are built only for dynamic matches
- body parsing skips
GETandDELETE - body parsing skips requests without
content-lengthortransfer-encoding
Requests that do not use those features do not pay for them.
Reduced allocation pressure
Allocation behavior is constrained, not eliminated:
- route definitions are compiled into stable lookup structures during registration
- the body parser keeps a
singleChunkfast path and only falls back toBuffer.concat()for multi-chunk bodies - synchronous handlers return directly without mandatory promise normalization
Allocations still exist in req.params, req.query, JSON serialization, and response buffering. The reduction is in framework-level overhead, not in total allocation count reaching zero.
Fewer required async transitions
The router checks whether a handler result is promise-like before awaiting it. Synchronous handlers stay on a shorter control path than designs that force all handlers through the same async abstraction.
Pipeline comparison
votrix removes parts of the default pipeline that are common in broader frameworks. The reduction is structural: fewer runtime layers, fewer mandatory transformations, and less conditional machinery on simple requests.
graph TD
subgraph Votrix
A1[Req] --> B1[Direct Router]
B1 --> C1[Handler]
C1 --> D1[Serializer]
D1 --> E1[Res]
end
subgraph Broader Runtime
A2[Req] --> B2[General Middleware]
B2 --> C2[Route Resolution]
C2 --> D2[Handler]
D2 --> E2[Serialization Layer]
E2 --> F2[Res]
endFastify and Express comparison
The benchmark data shows four consistent outcomes:
votrixleads Fastify and Express in RPS across all measured scenarios- the lead is largest on small JSON and direct routing paths
- Fastify remains closer on the simplest route
- Express stays behind on all four scenarios
The comparison supports one technical statement: a narrower runtime with fewer built-in stages can outperform broader frameworks on equivalent request/response contracts.
5. Benchmarks
Benchmark runner: benchmarks/run.ts
Load generator: autocannon
Execution model:
- each framework runs in an isolated child process
- each scenario is validated before measurement
- process CPU and RSS are sampled every
250 ms - artifacts are written to
benchmarks/results
Scenario set:
health:GET /healthuser-detail:GET /users/42?active=truecreate-user:POST /userswith a small JSON bodybulk-create-users:POST /users/bulkwith a larger JSON body
Shared benchmark conditions:
- host
127.0.0.1 - 2 repetitions per scenario
- 2 seconds warmup
- 5 seconds measured duration
- 100 connections
- pipelining
1
Latest artifact timestamp: 2026-04-03T03:18:21.722Z
Environment:
- Node
v24.13.1 - Windows 64-bit
- AMD Ryzen 5 5625U with Radeon Graphics
- 12 CPU threads
16540803072bytes total memory
The persisted artifact stores process.platform = "win32". That value is Node's platform identifier for Windows and does not indicate a 32-bit operating system.
Recorded metrics:
- requests per second
- average latency
- p95 latency
- p99 latency
- throughput
- average CPU
- peak CPU
- peak RSS
p95 is derived by interpolation between p90 and p97.5 when not exposed directly by autocannon. The persisted artifact does not include p50. No median is inferred.
Requests per second
| Scenario | Votrix | Fastify | Express |
| --- | ---: | ---: | ---: |
| health | 30124.80 | 27430.40 | 16486.81 |
| user-detail | 27075.20 | 22908.80 | 15361.60 |
| create-user | 20324.80 | 12754.40 | 11772.80 |
| bulk-create-users | 12635.20 | 10367.20 | 9812.81 |
Latency
| Scenario | Framework | Avg Latency | p95 | p99 |
| --- | --- | ---: | ---: | ---: |
| health | Votrix | 3.04 ms | 3.33 ms | 4.50 ms |
| health | Fastify | 3.10 ms | 4.33 ms | 5.50 ms |
| health | Express | 5.40 ms | 6.00 ms | 7.00 ms |
| user-detail | Votrix | 3.12 ms | 3.83 ms | 4.00 ms |
| user-detail | Fastify | 3.90 ms | 7.67 ms | 10.00 ms |
| user-detail | Express | 6.07 ms | 6.67 ms | 7.50 ms |
| create-user | Votrix | 4.26 ms | 5.67 ms | 7.50 ms |
| create-user | Fastify | 7.38 ms | 15.17 ms | 21.50 ms |
| create-user | Express | 8.12 ms | 8.83 ms | 9.50 ms |
| bulk-create-users | Votrix | 7.55 ms | 11.17 ms | 12.00 ms |
| bulk-create-users | Fastify | 9.26 ms | 18.50 ms | 24.50 ms |
| bulk-create-users | Express | 9.61 ms | 10.33 ms | 11.50 ms |
Delta to leader
| Scenario | Leader | Fastify vs Leader | Express vs Leader |
| --- | --- | ---: | ---: |
| health | Votrix | -8.94% | -45.27% |
| user-detail | Votrix | -15.39% | -43.26% |
| create-user | Votrix | -37.25% | -42.08% |
| bulk-create-users | Votrix | -17.95% | -22.34% |
Result reading
votrixleads all four measured scenarios- the largest gaps appear on JSON-heavy
POSTpaths - Fastify is closest on
GET /health - Express has the lowest throughput in every scenario
The benchmark scope is local loopback HTTP without TLS, reverse proxies, or external network latency.
6. Trade-offs
Performance is obtained by excluding broader framework behavior from the default path.
- fewer built-in abstractions
- less ergonomic lifecycle control than larger frameworks
- no plugin runtime
- no schema validation layer
- no specialized serialization subsystem
- minimal JSON body parser that does not inspect
Content-Type
Error handling also follows the same constraint. Middleware arity drives control flow. That keeps the runtime compact and explicit, but less expressive than larger framework lifecycles.
7. Technical Conclusion
The runtime is centered on three low-level decisions:
- static routes resolve through direct
Maplookups - query and param work is deferred until needed
- handler results go through a small terminal serializer
The benchmark artifact dated 2026-04-03T03:18:21.722Z shows higher throughput than Fastify and Express across the measured scenarios under the same request contracts and load settings. The observed gain is consistent with the implementation: less routing overhead, less conditional parsing, and fewer mandatory runtime stages per request.
