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

hono-typed-router

v0.2.0

Published

Path-typed router builder for Hono with composable middleware contexts and per-route policy hooks.

Readme

hono-typed-router

Path-typed router builder for Hono with composable middleware contexts and per-route policy hooks. Built on top of @hono/zod-openapi.

  • Typed paths — route paths flow through the type system; child routes inherit the parent's path and its accumulated context variables.
  • Composable contexts — attach middleware via .middleware<Vars>(...) and the new variables become available to every route under that context, with type-level guards against redeclaration.
  • Per-route policy hook — register a routeMiddleware factory once on the router; it runs against every declared route with full access to the resolved RouteConfig. Drop-in spot for scope checks, audit logging, rate limits, anything cross-cutting.
  • OpenAPI built-in — every route is declared via createRoute; the resulting app has full OpenAPI metadata.

Install

npm i hono-typed-router hono @hono/zod-openapi zod

Peer deps: hono ^4.12, @hono/zod-openapi ^1.1, zod ^4.

Quick start

import { Hono } from 'hono';
import { z } from 'zod';
import {
  createRouter,
  defineRootRoute,
  defineChildRoute,
  makeHonoResponse,
} from 'hono-typed-router';

const okResponse = makeHonoResponse(z.object({ ok: z.boolean() }), 'OK');

// 1. Define the root context — base path + global middlewares.
const rootRoute = defineRootRoute('/api', []);

// 2. Build a router factory. Hooks attached here apply to every router built from it.
const makeRouter = createRouter();

// 3. Declare a child context. The path is concatenated at the type level.
const thingsRoute = defineChildRoute<typeof rootRoute>()('/things');

// 4. Build the router. `route()` declares + returns a RouteConfig; `router.openapi()`
//    registers the handler.
const thingsRouter = makeRouter(thingsRoute, ({ router, route }) => {
  const list = route('get', { responses: { 200: okResponse } });
  router.openapi(list, (c) => c.json({ ok: true }));
  return router;
});

// 5. Mount children under the root and expose the final Hono app.
const app = makeRouter(rootRoute, ({ router }) => router, [thingsRouter])();
// GET /api/things -> { ok: true }

Concepts

defineRootRoute(path, middlewares)

Creates the root RouteContext. The path becomes the base path of any router built from this context, and the middlewares array runs on every request that hits routers under it.

defineChildRoute<typeof parent>()(path)

Creates a child RouteContext. The child's path type is ${ParentPath}${ChildPath}; its variables are inherited from the parent. Children are mounted on the parent in makeRouter(parent, factory, [child]).

.middleware<NewVars>(handler)

Adds a middleware to the context and surfaces any new variables it sets on c.var to subsequent middleware and route handlers. Redeclaring an existing variable is a type error.

type SessionVar = { session: { userId: string } };

const authed = defineRootRoute('/api', []).middleware<SessionVar>(async (c, next) => {
  c.set('session', await loadSession(c));
  await next();
});

const meRoute = defineChildRoute<typeof authed>()('/me');
//  routes built from meRoute see `c.var.session` typed.

createRouter({ routeMiddleware? })

Returns a makeRouter. Optional routeMiddleware is a factory (or array of factories) of the form (route: RouteConfig) => MiddlewareHandler. Each factory is invoked once at route declaration with the resolved RouteConfig; the returned middleware is attached to the route's exact method + path.

const makeRouter = createRouter({
  routeMiddleware: [
    (route) => async (c, next) => {
      console.log('hit', route.method, route.path);
      await next();
    },
  ],
});

Use array form to compose multiple concerns (scope check + request log + audit). Each middleware can call next() to continue or return a Response to short-circuit, exactly like a regular Hono middleware.

base — shared RouteConfig fragment

A partial RouteConfig deep-merged into every route declared by this router. Per-route values win on key conflicts; arrays (e.g. security, tags) are concatenated and structurally deduplicated; two zod schemas at the same position are unioned (base.or(route)) so a per-route 422 schema is combined with the base 422 schema rather than replacing it. The merged shape is reflected in the static type returned by route(), so handlers see the combined responses/request schema (with ZodUnion<[base, route]> at colliding schema slots).

const unauthorized = makeHonoResponse(z.object({ error: z.string() }), 'Unauthorized');

const makeRouter = createRouter({
  base: { responses: { 401: unauthorized } },
});

makeRouter(rootRoute, ({ router, route }) => {
  const list = route('get', { responses: { 200: okResponse } });
  // `list.responses` is typed with both `200` and `401`.
  router.openapi(list, (c) => c.json({ ok: true }));
  return router;
});

transformRoute — runtime-only config transformer

A hook of the form (config: RouteConfig) => RouteConfig. It runs immediately after the resolved RouteConfig is produced (and after any base merge), and before routeMiddleware factories see it. The static type of the returned config is unchanged — this is purely a runtime escape hatch for cross-cutting mutations (auto-tagging, injecting metadata, normalizing security entries, etc.).

const makeRouter = createRouter({
  transformRoute: (route) => ({
    ...route,
    tags: [...(route.tags ?? []), `${route.method}:${route.path}`],
  }),
});

route(method, config)

Inside a router factory, route() builds and returns a createRoute() config with its path locked to the context's path. The returned config feeds straight into router.openapi(config, handler).

Scope-check helper

A common use of routeMiddleware is enforcing OAuth-style scopes declared on a route's security. Use the sub-entry helper:

import { createRouter } from 'hono-typed-router';
import { createScopeMiddleware } from 'hono-typed-router/scopes';

const makeRouter = createRouter({
  routeMiddleware: createScopeMiddleware({
    resolve: (c) => c.var.session.scopes,
    // optional: customize the 403 body
    onForbidden: (missing) => ({ code: 'FORBIDDEN', missing }),
  }),
});

Behavior:

  • Extracts required scopes from every entry in route.security, flattening across schemes and deduplicating.
  • If route.security is absent or empty, the middleware is a no-op.
  • If any required scope is missing from resolve(c), returns 403 with the configured body.

Helpers

These mirror the shape @hono/zod-openapi expects:

import {
  makeHonoResponse,
  makeHonoJsonBody,
  makeHonoJsonRequest,
  makeHonoNoContentResponse,
} from 'hono-typed-router';

const route = createRoute({
  method: 'post',
  path: '/things',
  request: makeHonoJsonRequest(CreateBody, 'Create a thing'),
  responses: {
    200: makeHonoResponse(Thing, 'The created thing'),
    204: makeHonoNoContentResponse('Deleted'),
  },
});

Recipes

Binding a repository to a path parameter

The common "load :id once, expose it on c.var" pattern is a thin wrapper around .middleware:

import type { RouteContext } from 'hono-typed-router';
import type { ParamKeys } from 'hono/types';

const bindOrganization = <P extends string, V extends object>(
  ctx: RouteContext<P, V>,
  param: ParamKeys<P>,
) =>
  ctx.middleware<{ organization: Organization }>(async (c, next) => {
    const id = c.req.param(param as string);
    const organization = await organizationsRepository.findById(id);
    if (!organization) return c.json({ error: 'NOT_FOUND' }, 404);
    c.set('organization', organization);
    await next();
  });

const orgRoute = bindOrganization(
  defineChildRoute<typeof rootRoute>()('/organizations/:organizationId'),
  'organizationId',
);

Multiple routeMiddleware hooks

const makeRouter = createRouter({
  routeMiddleware: [
    createScopeMiddleware({ resolve: (c) => c.var.session.scopes }),
    (route) => async (c, next) => {
      const start = Date.now();
      await next();
      logger.info({ method: route.method, path: route.path, ms: Date.now() - start });
    },
  ],
});

Documentation

  • docs/usage.md — copy-pastable examples: CRUD resources, nested children, serving, OpenAPI UI, opt-outs, testing.
  • docs/api.md — full signature reference.
  • docs/design.md — rationale and deliberate omissions.

License

MIT