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

@fresho/router

v0.3.2

Published

Type-safe route definitions with auto-generated clients and OpenAPI docs

Readme

@fresho/router

Type-safe routing for Cloudflare Workers, Deno, Bun, and Node.js. Define routes once, get validated handlers, typed clients, and OpenAPI docs.

~2KB gzipped. Zero dependencies.

import { route, router } from "@fresho/router";

const api = router({
	health: router({
		get: async () => ({ status: "ok" }),
	}),

	users: router({
		// GET /users - list with optional limit
		get: route({
			query: { limit: "number?" },
			handler: async (c) => db.users.list(c.query.limit),
		}),

		// POST /users - create user
		post: route({
			body: { name: "string", email: "string" },
			handler: async (c) => db.users.create(c.body),
		}),

		// GET /users/:id - get by id
		$id: router({
			// Bare function shorthand instead of `route` type
			get: async (c) => db.users.get(c.path.id),
		}),
	}),
});

// Cloudflare Worker / Deno / Bun
export default { fetch: api.handler() };

Features

  • Type-safe path params$id creates dynamic segments, typed via route.ctx<>()
  • Schema validation — query and body validated at runtime, typed at compile time
  • Property-based routing — property names become URL segments
  • Typed HTTP client — call your API with full type safety
  • Typed local client — test handlers directly without HTTP
  • OpenAPI generation — auto-generate docs from your routes
  • Middleware — cors, auth, rate limiting, and more
  • Streaming — SSE and JSON lines helpers
  • HTTP-compliant — automatic HEAD support for GET routes
  • Zero dependencies — just Web APIs

Installation

npm install @fresho/router

Path Convention

Property names become URL path segments:

router({
  api: router({           // /api
    users: router({       // /api/users
      get: async () => ...,
      $id: router({       // /api/users/:id
        get: async (c) => c.path.id,
      }),
    }),
  }),
});
  • Regular properties → static segments (users/users)
  • $param properties → dynamic segments ($id/:id)
  • HTTP methods (get, post, etc.) → handlers at that path

Schemas

Define query and body schemas using shorthand syntax:

// Primitives
{ name: 'string' }      // required string
{ age: 'number' }       // required number (coerced from string in query)
{ active: 'boolean' }   // required boolean (accepts "true"/"false"/"1"/"0")

// Optional (append ?)
{ name: 'string?' }     // optional string

// Arrays
{ tags: 'string[]' }    // string array
{ scores: 'number[]' }  // number array

// Nested objects
{
  address: {
    street: 'string',
    city: 'string',
    zip: 'number?'
  }
}

Types are automatically inferred:

post: route({
	body: {
		title: "string",
		tags: "string[]",
		metadata: { priority: "number", draft: "boolean?" },
	},
	handler: async (c) => {
		c.body.title; // string
		c.body.tags; // string[]
		c.body.metadata; // { priority: number, draft: boolean | undefined }
	},
});

Typing Best Practices

Schemas (query/body) provide runtime validation AND type inference:

get: route({
	query: { limit: "number?" },
	handler: async (c) => c.query.limit, // number | undefined
});

Context (route.ctx<T>()) provides types only for things schemas don't cover:

interface MyContext {
	path: { id: string }; // from $id segment
	env: { DB: Database }; // runtime environment
	user: { name: string }; // from auth middleware
}

get: route.ctx<MyContext>()({
	query: { include: "string?" }, // validated
	handler: async (c) => ({
		id: c.path.id, // from context
		user: c.user.name, // from context
		include: c.query.include, // from schema
	}),
});

Don't add explicit type annotations to handlers — let types flow from schemas and context:

// GOOD: types inferred
handler: async (c) => c.query.limit;

// BAD: redundant annotation
handler: async (c: { query: { limit?: number } }) => c.query.limit;

Nested Routers

Compose routers:

const users = router({
  get: async () => db.users.list(),
  post: route({
    body: { name: 'string' },
    handler: async (c) => db.users.create(c.body),
  }),
  $id: router({
    get: async (c) => db.users.get(c.path.id),
    delete: async (c) => db.users.delete(c.path.id),
  }),
});

const api = router({
  users,
  posts: router({ ... }),
});

// Routes:
// GET    /users
// POST   /users
// GET    /users/:id
// DELETE /users/:id

Middleware

Add middleware to routers:

import { router, route } from "@fresho/router";
import { cors, errorHandler, jwtAuth } from "@fresho/router/middleware";

const api = router(
	{
		hello: router({
			get: async () => ({ message: "world" }),
		}),
	},
	cors(),
	errorHandler(),
	jwtAuth({ secret: process.env.JWT_SECRET, claims: (p) => ({ user: p.sub }) })
);

Built-in middleware: cors, errorHandler, logger, rateLimit, requestId, timeout, basicAuth, bearerAuth, jwtAuth, contentType.

See Middleware Documentation for detailed usage.

Authentication Utilities

For standalone auth utilities (JWT signing/verification, Basic auth, OAuth), import from @fresho/router/auth:

import { jwtSign, jwtVerify, parseBasicAuth } from "@fresho/router/auth";

// Sign a JWT
const token = await jwtSign({ uid: "user-123" }, secret, { expiresIn: "1h" });

// Verify a JWT
const payload = await jwtVerify(token, secret);

// Parse Basic auth header
const creds = parseBasicAuth(request.headers.get("Authorization"));

Available utilities:

  • JWT: jwtSign, jwtVerify
  • Basic Auth: parseBasicAuth, encodeBasicAuth, extractBasicAuthToken
  • OAuth: encodeOAuthState, decodeOAuthState, exchangeCode, refreshAccessToken, revokeToken, buildAuthorizationUrl, OAUTH_PROVIDERS

See Authentication Documentation for detailed usage.

HTTP Client

Generate a typed client for your API:

// === Server (api.ts) ===
import { route, router } from "@fresho/router";

export const api = router({
	users: router({
		get: route({
			query: { limit: "number?" },
			handler: async (c) => ({ users: [], limit: c.query.limit }),
		}),
		$id: router({
			get: async (c) => ({ id: c.path.id, name: "Alice" }),
		}),
	}),
});

// === Client ===
import { createHttpClient } from "@fresho/router";
import type { api } from "./api"; // Type-only import!

const client = createHttpClient<typeof api>({
	baseUrl: "https://api.example.com",
});

// Direct call for implicit GET
const users = await client.users({ query: { limit: 10 } });

// Path params
const user = await client.users.$id({ path: { id: "123" } });
// user is typed as { id: string, name: string }

// Explicit HTTP methods use $-prefix
await client.users.$get();                    // GET /users
await client.users.$post({ body: { name: "Bob" } }); // POST /users

HTTP Method Syntax

HTTP methods are prefixed with $ to distinguish them from path navigation:

// $-prefixed = execute HTTP method
client.users.$get();     // GET /users
client.users.$post({});  // POST /users
client.users.$put({});   // PUT /users
client.users.$delete();  // DELETE /users

// No prefix = navigate to path segment
client.api.get.$get();   // GET /api/get (navigate to "get", then execute GET)

This allows routes with path segments named after HTTP methods (e.g., /api/get, /resources/delete).

Local Client

Test handlers directly without HTTP overhead:

import { createLocalClient } from "@fresho/router";

const client = createLocalClient(api);
client.configure({ env: { DB: mockDb } });

const user = await client.users.$id({ path: { id: "123" } });
assert.equal(user.name, "Alice");

OpenAPI Generation

Generate OpenAPI 3.0 documentation:

import { generateDocs } from "@fresho/router";

const spec = generateDocs(api, {
	title: "My API",
	version: "1.0.0",
});

// Serve at /openapi.json
const docs = router({
	openapi: router({
		get: async () => spec,
	}),
});

Streaming

Server-Sent Events

import { sseResponse } from "@fresho/router";

events: router({
	get: async () =>
		sseResponse(async (send, close) => {
			send({ data: "connected" });
			send({ event: "update", data: { count: 1 } });
			close();
		}),
});

JSON Lines

import { streamJsonLines } from "@fresho/router";

logs: router({
	get: async () =>
		streamJsonLines(async (send, close) => {
			send({ level: "info", message: "Starting..." });
			send({ level: "info", message: "Done" });
			close();
		}),
});

Cloudflare Workers

import { route, router } from '@fresho/router';

const api = router({ ... });

export default {
  fetch: api.handler(),
};

Typed Context

Use route.ctx<T>() for environment bindings and middleware-added properties:

interface AppContext {
	env: { DB: D1Database };
	user: { id: string }; // from auth middleware
}

const api = router(
	{
		profile: router({
			get: route.ctx<AppContext>()({
				handler: async (c) => {
					const data = await c.env.DB.prepare("...").all();
					return { userId: c.user.id, data };
				},
			}),
		}),
	},
	jwtAuth({
		secret: (c) => c.env.JWT_SECRET,
		claims: (p) => ({ user: { id: p.sub } }),
	})
);

export default { fetch: api.handler() };

HEAD Requests

Per RFC 9110, HEAD requests are automatically handled for any GET route:

users: router({
	get: async () => ({ users: await db.getUsers() }),
});

// GET /users  → 200 with body
// HEAD /users → 200 with no body (same headers)

Define an explicit head handler if you need different behavior:

users: router({
	head: async () => new Response(null, { headers: { "X-Count": "100" } }),
	get: async () => ({ users: await db.getUsers() }),
});

Size

| Usage | Minified | Gzipped | | --------------------------- | -------- | ------- | | Core (routing + validation) | 4.2 KB | 1.9 KB | | + HTTP client | 5.8 KB | 2.4 KB | | + OpenAPI docs | 5.3 KB | 2.3 KB | | + cors, errorHandler | 6.5 KB | 2.7 KB | | + all middleware | 11.6 KB | 4.4 KB |

Tree-shakeable: only pay for what you import.

License

MIT