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

@gxcloud/router

v0.0.2

Published

An ultra-fast, zero-dependency Node.js HTTP router and controller framework with compile-time type-safety, lazy body parsing, and automatic OpenAPI generation via Valibot.

Readme

@gxcloud/router

An ultra-fast, zero-dependency Node.js HTTP router and controller framework with compile-time type-safety, lazy body parsing, and automatic OpenAPI generation via Valibot.

Personal project: This is not a competitor to Express, Fastify, Hono, or Elysia. It is purpose-built for the author's TypeScript backend projects — prioritizing control, simplicity, low overhead, and strong type inference.

Features

  • Single source of truth — Route definitions drive routing, validation, handler typing, and OpenAPI generation. No duplication.
  • Fluent builder API — Declarative chaining: .get(path).params(s).query(s).body(s).meta(m).handle(h).
  • Built-in server — Start serving with a single .listen() call. No node:http boilerplate.
  • Structured logging — Optional NDJSON request logging via .logger().
  • Radix Tree router — O(k) path matching. No linear scans, no regex-per-route.
  • Compile-time type safety — Handler context types are inferred from Valibot schemas.
  • Lazy body parsing — Request bodies are never read unless the route declares a body schema.
  • Valibot validation — Zero-dependency schema validation with excellent TypeScript inference.
  • Automatic OpenAPI 3.1 — Generated from route definitions. No separate YAML/JSON files.
  • Scalar UI support — Interactive API documentation with one flag.
  • Sub-router mounting — Compose APIs from isolated controllers. Flattened at compile time.
  • Route-scoped before-hooks — Execute logic before handlers with short-circuit support.
  • Response helpersctx.json(), ctx.text(), ctx.reply.created(), ctx.reply.noContent().
  • Custom error handler — Override error formatting globally.
  • Testing helperapp.inject() for zero-server HTTP testing.
  • No middleware stack — Route-scoped validation replaces global middleware.
  • No decorators — No reflect-metadata, no class scanning, no DI containers.
  • No dependency injection — Import services manually into controller modules.

Quick Start

import * as v from "valibot";
import { createRouter } from "@gxcloud/router";

createRouter()
  .get("/")
  .handle(() => ({ message: "Hello, world!" }))
  .get("/hello/:name")
  .params(v.object({ name: v.string() }))
  .query(v.object({ title: v.optional(v.string()) }))
  .meta({ summary: "Greet someone", tags: ["Greetings"] })
  .handle((ctx) => ({
    greeting: `Hello, ${ctx.params.name}!`,
    title: ctx.query.title,
  }))
  .post("/echo")
  .body(v.object({ message: v.string() }))
  .handle((ctx) => ({ youSaid: ctx.body.message }))
  .openapi("/openapi.json", { scalar: true })
  .logger()
  .listen(3000);

Core Concepts

Route as Source of Truth

A single .get("/users/:id").params(...).query(...).meta(...).handle(handler) call defines the path, validation, metadata, and handler. Everything derives from this declaration — the router, the TypeScript types, and the OpenAPI schema.

Build → Auto-Compile → Runtime

  1. Build — Declare routes with the fluent builder. Mutable, flexible.
  2. Auto-Compile — The first time a request arrives (via nodeHandler(), .listen(), or .match()) is called, the builder freezes itself, builds the Radix Tree, and generates OpenAPI. No explicit .compile() needed.
  3. Runtime — The compiled router serves requests with minimal overhead.

You can still call .compile() explicitly if you want to freeze the router early or access the CompiledRouter directly:

const compiled = app.compile();
console.log(compiled.openapiDocument); // already generated

Route Precedence

The Radix Tree enforces strict precedence:

  • Static segments (/users/me) beat parameter segments (/users/:id)
  • Parameter segments beat wildcard segments (/users/*)

Lazy Body Parsing

Bodies are parsed only when the route declares a body schema. GET routes, health checks, and metadata endpoints never pay the cost of body parsing.

Validation

Inbound data (params, query, body) is validated with Valibot. Outbound data is not validated at runtime — TypeScript provides the safety. This keeps responses fast.

API Reference

createRouter()

Returns a RouterBuilder.

Route Methods

Each returns a RouteBuilder for chaining:

  • .get(path)
  • .post(path)
  • .put(path)
  • .delete(path)
  • .patch(path)
  • .head(path)
  • .options(path)

Schema Methods

  • .params(schema) — Valibot schema for route parameters
  • .query(schema) — Valibot schema for query parameters
  • .body(schema) — Valibot schema for request body (enables body parsing)
  • .meta(metadata) — Route metadata for OpenAPI (summary, description, tags, deprecated, operationId)
  • .before(hook) — Register a before-hook (can modify context or short-circuit with a response)
  • .response(statusCode, description?, schema?) — Document response schemas for OpenAPI

Server (.listen())

Starts an HTTP server. No node:http imports required.

createRouter()
  .get("/hello")
  .handle(() => ({ message: "world" }))
  .listen(3000, () => console.log("running"));

Accepts the same arguments as http.Server.listen():

  • .listen(port, callback?)
  • .listen(port, hostname, callback?)
  • .listen(port, hostname, backlog, callback?)

Returns the http.Server instance for lifecycle control.

Logger (.logger())

Enables structured NDJSON request logging to stdout.

createRouter()
  .get("/hello")
  .handle(() => ({ message: "world" }))
  .logger()
  .listen(3000);

Each request produces a log line:

{"time":"2026-06-10T21:00:00.000Z","level":"info","method":"GET","path":"/hello","status":200,"duration":1.23}

Customize logged fields:

.logger({ fields: ["method", "path", "status"] })

Disable logging:

.logger({ enabled: false })

Default fields: time, level, method, path, status, duration, error.

Body Size Limit (.bodyLimit())

Controls the maximum request body size (in bytes). Defaults to 1 MB.

createRouter()
  .post("/upload")
  .body(v.object({ data: v.string() }))
  .handle((ctx) => ctx.body)
  .bodyLimit(5_242_880); // 5 MB

Custom Error Handler (.errorHandler())

Override error formatting for all error responses (404, 405, validation, internal errors).

createRouter()
  .get("/secure")
  .handle(() => { throw new Error("boom"); })
  .errorHandler((err) => ({
    status: 500,
    body: { custom: true, detail: err instanceof Error ? err.message : String(err) },
  }));

Testing Helper (.inject())

Test routes without starting a server, using a lightweight mock request/response:

const app = createRouter()
  .get("/ping")
  .handle(() => ({ pong: true }));

const res = await app.inject({ method: "GET", path: "/ping" });
// res.status, res.body, res.headers

Works with POST bodies, headers, query strings, and all route features.

Handler

.handle((ctx) => {
  // ctx.params  — validated params (or raw Record<string, string>)
  // ctx.query   — validated query (or raw Record<string, string | string[]>)
  // ctx.body    — validated body (or undefined)
  // ctx.raw     — { req, method, path }
  // ctx.json    — (data, status?) => Response  (JSON helper)
  // ctx.text    — (data, status?) => Response  (text helper)
  // ctx.reply.created  — (data?) => Response  (201)
  // ctx.reply.noContent — () => Response      (204)
  return { result: "ok" };
})

Sub-Router Mounting

const users = createRouter();
users.get("/:id").handle((ctx) => fetchUser(ctx.params.id));

const app = createRouter().mount("/users", users);

Mounted routes are flattened at compile time — no runtime overhead. Sub-routers are guarded: calling .listen() or .logger() on a mounted router throws an error.

OpenAPI

const app = createRouter()
  .get("/users/:id")
  .meta({ summary: "Get user", tags: ["Users"] })
  .params(v.object({ id: v.string() }))
  .handle(() => ({}))
  .openapi("/openapi.json", { scalar: true }); // Enable Scalar UI

Customize the document title and version:

.openapi("/openapi.json", { title: "My API", version: "2.0.0" })

Document response schemas per route:

.get("/items")
.response(200, "List of items", v.object({ items: v.array(v.string()) }))
.response(404, "Not found")
.handle(() => ({}))

The document is available at app.openapiDocument (triggers auto-compile if needed).

Explicit Compilation

.compile() is optional — the router auto-compiles on first use. Call it explicitly when you need to pass a CompiledRouter directly or enforce an early freeze:

const compiled = app.compile();
compiled.match("GET", "/hello");

Response Values

Handlers can return:

  • Objects → JSON { "Content-Type": "application/json" }
  • Strings → Text { "Content-Type": "text/plain" }
  • null / undefined → 204 No Content
  • Buffer → Binary { "Content-Type": "application/octet-stream" }
  • Response — Custom status/headers: new Response(body, status, headers)

Response helpers are available on the context for common patterns:

ctx.json({ data }, 201);              // Content-Type: application/json
ctx.text("created", 201);             // Content-Type: text/plain
ctx.reply.created({ id: 1 });         // 201 Created
ctx.reply.noContent();                // 204 No Content

Error Handling

Errors produce consistent JSON:

{
  "error": {
    "type": "validation_error",
    "status": 400,
    "message": "Request validation failed",
    "issues": [
      {
        "path": ["params", "id"],
        "message": "Invalid value",
        "code": "invalid_type"
      }
    ]
  }
}

Error types: validation_error (400), not_found (404), method_not_allowed (405), body_parse_error (400), unsupported_media_type (415), internal_error (500).

Use .errorHandler() to customize error formatting.

Examples

The examples/ directory contains:

  • basic.ts — Simple greeting and echo API
  • users-api.ts — CRUD users API with sub-router mounting

Run with: npx tsx examples/basic.ts

Benchmarks

Run with: npm run bench

The Radix Tree provides O(k) matching where k is the number of path segments — significantly faster than linear route scanning.

Philosophy

  • Explicit over magic — No decorators, no file scanning, no DI.
  • Simple over flexible — One way to do things. One validation library. One routing approach.
  • Fast over convenient — Lazy body parsing, compiled router, no middleware stacks.
  • Typed over dynamic — Types flow from schemas through handlers. Compile-time safety is a feature.

License

MIT