npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

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 404 and 500 responses

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 RouteNode objects with children, 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

  1. createServer receives (req, res).
  2. App.listen() forwards the pair into router.handleRequest(req, res).
  3. Router.decorateRequest() casts the Node request into FrameworkRequest and initializes req.params = {} and req.query = {}.
  4. Router.handle() reads req.url, computes the query boundary, and chooses between direct route dispatch and middleware execution.
  5. If middleware is present, Router.runMiddlewares() executes the chain in registration order.
  6. createParseBodyMiddleware() skips GET and DELETE, skips requests without body headers, parses JSON when a body exists, and forwards invalid JSON as 400 Invalid JSON.
  7. createRequestLoggerMiddleware() records timestamps and logs completion or early socket close without changing routing behavior.
  8. Router.dispatchRoute() normalizes the pathname and calls matchRoute(req.method, pathname).
  9. matchRoute() checks the method-specific static map first.
  10. If no static route matches, matchRoute() walks the RouteNode tree, preferring static children and falling back to dynamicChild.
  11. Query parsing runs only after a successful route match and only when ? exists in the raw URL.
  12. Route params are materialized only for dynamic matches.
  13. The resolved handler executes.
  14. If the handler returns a value and the response is still open, sendResponse() serializes the result.
  15. If the handler throws or calls next(error), runErrorMiddlewares() executes error middleware or falls back to 500 Internal Server Error.
  16. 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:

  1. Node invokes the server callback.
  2. App forwards the request to Router.handleRequest().
  3. decorateRequest() allocates empty params and query objects.
  4. Body parser middleware exits immediately because the method is GET.
  5. dispatchRoute() computes the pathname and detects that no query string is present.
  6. matchRoute() resolves the handler from staticRoutes.GET.get("/health").
  7. No dynamic tree traversal occurs.
  8. No query parsing occurs.
  9. No param materialization occurs.
  10. The handler returns a small object synchronously.
  11. sendResponse() sets Content-Type: application/json; charset=utf-8.
  12. JSON.stringify() serializes the object.
  13. 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 Map lookup
  • 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 GET and DELETE
  • body parsing skips requests without content-length or transfer-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 singleChunk fast path and only falls back to Buffer.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]
  end

Fastify and Express comparison

The benchmark data shows four consistent outcomes:

  • votrix leads 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 /health
  • user-detail: GET /users/42?active=true
  • create-user: POST /users with a small JSON body
  • bulk-create-users: POST /users/bulk with 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
  • 16540803072 bytes 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

  • votrix leads all four measured scenarios
  • the largest gaps appear on JSON-heavy POST paths
  • 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 Map lookups
  • 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.