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

router-bun

v1.2.8

Published

A fast, Express-like router for Bun.serve with native Socket.IO support.

Downloads

546

Readme

router-bun

MIT npm

A fast, Express-like router for bun.serve().

router-bun leverages Bun's bun.serve() to deliver a fast, familiar routing experience. It provides an Express-like API with built-in middleware for CORS, rate limiting, file uploads, and SSE. Includes a Socket.IO example for real-time applications.


📖 Extended docs: docs/getting-started.md · docs/router-api.md · docs/routing.md · docs/middleware.md · docs/request-response.md · docs/dump-stats.md


Install

npm i router-bun

Quick Start

import { Router } from "router-bun";

const router = new Router();

router.get("/", ({ res }) => {
  res.send("Hello from router-bun!");
});

Bun.serve({ fetch: router.handle });

With WebSocket support

import { Router } from "router-bun";

const router = new Router();

router.get("/", ({ res }) => res.send("Hello!"));
router.ws("/ws");

Bun.serve({
  fetch: router.handle,
  websocket: {
    open(ws) { console.log("WS open"); },
    message(ws, msg) { ws.send(msg); },
    close(ws) { console.log("WS closed"); },
  },
});

Router API

Creating a Router

const router = new Router();

Route Methods

All methods return the Router instance for chaining.

| Method | Signature | |--------|-----------| | get | (path, handler, ...handlers) | | post | (path, handler, ...handlers) | | put | (path, handler, ...handlers) | | delete | (path, handler, ...handlers) | | patch | (path, handler, ...handlers) | | options | (path, handler, ...handlers) | | head | (path, handler, ...handlers) | | trace | (path, handler, ...handlers) | | connect | (path, handler, ...handlers) | | use | (method, path, handler, ...handlers) — catch-all method |

Naming handlers

import { handlerName } from "router-bun";

router.get("/users/:id", handlerName("getUser", ({ req, res }) => {
  res.send("ok");
}));
// getRouteDefinitions() → handlerName: "getUser"

Route metadata (query params for Swagger/OpenAPI)

router.get("/search", handlerName("search", handler));
router.describe("/search", {
  queryParams: [
    { name: "q",     type: "string",  required: true,  description: "Search query" },
    { name: "limit", type: "integer", required: false, default: 20 },
  ],
});
// getRouteDefinitions() → queryParams populated

Testing

import { createTestContext } from "router-bun";

const ctx = createTestContext({
  method: "GET",
  url: "/users/42?filter=active",
  pathParams: { id: "42" },
});
myHandler(ctx);
expect(ctx.res.statusCode).toBe(200);

Route Groups

router.group("/api", (r) => {
  r.get("/users", handler);
  r.post("/users", handler);
});
// Registers: GET /api/users, POST /api/users

Sub-router Mounting

const sub = new Router();
sub.get("/items", handler);

router.mount("/api/v1", sub);
// Registers: GET /api/v1/items

Error Handling

router.onError((err, { req, res }) => {
  console.error(err);
  res.status(500).json({ error: err.message });
});

WebSocket

router.ws("/ws");
router.setWebSocketHandlers({
  open(ws) {},
  message(ws, msg) {},
  close(ws) {},
});

Request Testing

const res = await router.request("/api/users", { method: "GET" });

Static Helpers

Router.parseCookies(req);
Router.storeCookies(req, res);
Router.getFile(req, "avatar");
Router.getFiles(req, "photos");
Router.getFileFieldNames(req);
Router.getFormFields(req);

Routing

Path Patterns

| Pattern | Description | Example Match | |---------|-------------|---------------| | /users/:id | Named parameter | /users/42 | | /files/* | Single wildcard (matches one segment) | /files/doc | | /files/** | Double wildcard (matches zero or more) | /files/a/b/c | | /static/** | Serve all nested paths | /static/css/main.css |

Named Parameters

router.get("/users/:id/posts/:postId", ({ req, res }) => {
  const id = req.pathParam("id").int();      // number | undefined
  const postId = req.pathParam("postId").require();  // string (throws if missing)
  res.json({ id, postId });
});

Query Parameters

router.get("/search", ({ req, res }) => {
  const q = req.queryParam("q").string();
  const page = req.queryParam("page").int() ?? 1;
  const sort = req.queryParam("sort").enum(["asc", "desc"]) ?? "asc";
  res.json({ q, page, sort });
});

Type helper: Param

Both req.queryParam() (query) and req.pathParam() (path) return a Param instance with typed accessor methods:

| Method | Returns | Description | |--------|---------|-------------| | .string(default?) | string \| undefined | Value as string (or default) | | .int(default?) | number \| undefined | Integer value (or default) | | .number(default?) | number \| undefined | Numeric value (or default) | | .numberBetween(min, max, default?) | number \| undefined | Clamped number (or default) | | .boolean(default?) | boolean \| undefined | "true"/"1"true, "false"/"0"false (or default) | | .enum(allowed, default?) | T \| undefined | Only returns if value matches allowed set (or default) | | .require(name?) | string | Returns value or throws | | .exists() | boolean | Whether the param is present | | .or(default) | string | Value or fallback default | | .array() | string[] | All values as array | | .rawValue() | string \| string[] \| undefined | Unprocessed raw value |

All getters accept an optional default value — no more ??:

const page   = req.queryParam("page").number(1);   // 1 if missing/invalid
const sort   = req.queryParam("sort").string("asc"); // "asc" if missing
const active = req.queryParam("active").boolean(true); // true if missing

Middleware

Built-in Middleware

All built-in middleware can be applied as direct router methods:

router.cors("*", "/**", { origin: "*" });
router.body("*", "/api/*", { json: true, form: true });
router.rateLimit("POST", "/api/*", { max: 10, windowMs: 60000 });
router.requestId("*", "/**", { header: "X-Trace-Id" });
router.timeout("POST", "/api/upload", { timeoutMs: 5000 });
router.fileUpload("POST", "/upload", { maxSize: 10_000_000 });
router.cookies("*", "/**", true);   // auto-response headers

CORS

interface CorsOptions {
  origin?: string | string[] | ((origin: string) => string | undefined);
  methods?: string[];           // default: all common methods
  allowedHeaders?: string[];
  exposedHeaders?: string[];
  credentials?: boolean;
  maxAge?: number;              // default: 86400
  preflightContinue?: boolean;
}

Rate Limiting

interface RateLimitOptions {
  max: number;                           // max requests
  windowMs: number;                      // time window in ms
  keyGenerator?: (req) => string;        // default: x-forwarded-for or x-real-ip
  message?: string;                      // default: "Too many requests"
  headers?: boolean;                     // default: true
}

Rate limit headers: X-RateLimit-Limit, X-RateLimit-Remaining, X-RateLimit-Reset, Retry-After.

Body Parser

interface BodyParserOptions {
  json?: boolean;    // default: true
  text?: boolean;    // default: true
  form?: boolean;    // default: true
  limit?: number;    // max bytes
}

Parsed body is available at req.parsedBody.

File Upload

interface FileUploadOptions {
  maxSize?: number;        // max file size in bytes
  allowedTypes?: string[]; // allowed MIME types
}

After parsing, use static helpers to access files:

Router.getFile(req, "avatar");       // UploadedFile | undefined
Router.getFiles(req, "photos");      // UploadedFile[]
Router.getFileFieldNames(req);       // string[]
Router.getFormFields(req);           // Record<string, string>
interface UploadedFile {
  name: string;
  type: string;
  size: number;
  arrayBuffer(): Promise<ArrayBuffer>;
  text(): Promise<string>;
  json(): Promise<unknown>;
  blob(): Promise<Blob>;
  stream(): ReadableStream;
}

Server-Sent Events (SSE)

import { createSSEStream, sse } from "router-bun";

router.get("/events", ({ res }) => {
  const stream = createSSEStream(res);
  stream.sendEvent("message", "Hello");
  stream.send({ event: "update", data: JSON.stringify({ key: "val" }) });
});
interface SSEStream {
  send(message: SSEMessage): void;
  sendEvent(event: string, data: string | string[], id?: string): void;
  sendComment(comment: string): void;
  close(): void;
  isOpen(): boolean;
}

Static Files

router.static("/**", "./public", "index.html", 10);

Parameters: path, targetDir, indexFile (default: index.html), deepestLevel (default: 10).

Supports ETag caching with 304 Not Modified responses.

Redirect

router.redirect("*", "/old-path", "/new-path");
router.redirect("*", "/old", "/new", true); // permanent (308)

Cookie Parsing

router.cookies("*", "/**", true); // auto-response headers

// In handlers:
req.cookies.session = "abc123";   // set
req.cookies.session = undefined;  // delete

Request ID

interface RequestIdOptions {
  header?: string;    // default: "X-Request-Id"
  generator?: (req) => string;  // default: crypto.randomUUID()
}

Timeout

interface TimeoutOptions {
  timeoutMs: number;
  message?: string;   // default: "Request timeout"
}

Sends 408 Request Timeout if the handler exceeds the time limit.


Request & Response

Enhanced Request (req)

The request object in handlers extends Bun's built-in Request with:

| Property | Description | |----------|-------------| | req.path | URL pathname | | req.method | HTTP method string | | req.httpMethod | HttpMethod enum | | req.ip | Client IP | | req.ips | IP chain | | req.id | Request ID (if requestId middleware used) | | req.parsedBody | Parsed request body (if bodyParser used) | | req.cookies | Parsed cookies object (if cookies middleware used) | | req.pathParams | Matched path parameters | | req.splitPath | Path split into segments | | req.queryParams | Query string parameters | | req.server | Bun server reference | | req.sock | Socket address |

Response Builder (res)

| Method | Description | |--------|-------------| | res.send(body) | Send response | | res.json(data, code?) | JSON response | | res.text(data, code?) | Plain text response | | res.html(data, code?) | HTML response | | res.sendFile(file, code?) | Send a BunFile | | res.sendRedirect(url, perma?) | 307 or 308 redirect | | res.sendError(msg, code?) | JSON error response | | res.status(code, text?) | Set status code | | res.setHeader(name, value) | Set response header | | res.setCookie(name, value, opts?) | Set cookie | | res.unsetCookie(name) | Expire cookie | | res.beforeSent(hook) | Add pre-send hook | | res.build() | Build final Response |

Context (ctx)

The context object passed to all handlers provides:

| Property/Method | Description | |----------------|-------------| | ctx.req | Enhanced request | | ctx.res | Response builder | | ctx.data | Extensible per-request data store | | ctx.set(key, value) | Store data | | ctx.get(key) | Retrieve data | | ctx.status(code) | Set status | | ctx.json(data, code?) | JSON response | | ctx.text(body, code?) | Text response | | ctx.html(body, code?) | HTML response | | ctx.redirect(url, code?) | Redirect | | ctx.notFound(msg?) | 404 response | | ctx.error(msg, code?) | Error response |

Type Safety with ContextDataMap

Augment ContextDataMap to get auto-inferred types:

declare module "router-bun" {
  interface ContextDataMap {
    user: { id: string; role: "admin" | "user" };
  }
}

// ctx.get("user") now returns UserData | undefined
// ctx.set("user", ...) is type-checked

Socket.IO Example

A complete Socket.IO chat server is available in examples/socket-io/. It implements the Engine.IO and Socket.IO wire protocols on top of router-bun's WebSocket support and works with the standard socket.io-client on the frontend.

cd examples/socket-io
bun run server.ts

See the example source for details.


Route Dump & Stats

Route Table

console.info(router.dump(server));
// Outputs a formatted table of all routes

Route Listing

const routes = router.getRoutes();          // excludes middleware
const allRoutes = router.getRoutes(true);   // includes middleware

Structured Route Definitions (for Swagger/OpenAPI)

const defs = router.getRouteDefinitions();
for (const def of defs) {
  def.method;        // "GET"
  def.path;          // "/users/:id"
  def.pathParams;    // [{ name: "id", type: "named", position: 1 }]
  def.queryParams;   // [{ name: "q", type: "string", required: true, description: "..." }]
  def.handlerName;   // "getUser"
  def.middlewareChain;
  def.stats;         // performance stats (if tracked)
}

Path params (:id, *, **) are auto-discovered from route patterns. Query params are declared via router.describe():

router.get("/search", handlerName("search", handler));
router.describe("/search", {
  queryParams: [
    { name: "q",     type: "string",  required: true,  description: "Search query" },
    { name: "limit", type: "integer", required: false, default: 20 },
    { name: "sort",  type: "string",  required: false, enum: ["asc", "desc"] },
  ],
});

Use this to generate OpenAPI specs:

const swaggerPath = def.path.replace(/:(\w+)/g, "{$1}");
// "/users/:id" → "/users/{id}"

// Merge with def.queryParams for full parameter list

Performance Stats

import { trackRouteTime, getRouteStats, clearRouteStats } from "router-bun";

trackRouteTime("GET", "/users", 12.5);
const stats = getRouteStats();   // Map<string, RouteStats>
clearRouteStats();
interface RouteStats {
  requestCount: number;
  totalTimeMs: number;
  avgTimeMs: number;
}

When stats are recorded, router.dump() includes request count and average response time per route.


Examples

| Example | Description | |---------|-------------| | simple | Basic CRUD server | | auth-middleware | Auth & role-based authorization | | path-params | Wildcard and named parameter patterns | | websocket | WebSocket upgrade | | cookies | Cookie set/get/clear | | redirect | URL redirection | | static-serve | Static file serving | | chat-demo | Full demo: auth, file upload, WebSocket chat, groups | | socket-io | Socket.IO chat example (rooms, typing, online users) |


TypeScript

This library is written in TypeScript and ships with full type definitions.

Module Augmentation

declare module "router-bun" {
  interface ContextDataMap {
    user: { id: string; role: "admin" | "user" };
  }
}

Tests

bun test

License

MIT