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

@routepact/hono

v0.1.22

Published

Hono adapter for type-safe route pacts - endpoint builders, request/response validation middleware, and router setup

Readme

@routepact/hono

Hono adapter for @routepact/core pacts. Provides a fluent router builder, automatic request/response validation, and support for both typed and native Hono middleware.

Installation

npm install @routepact/hono @routepact/core hono

You also need a schema library that implements the Standard Schema interface (e.g. Zod, Valibot, ArkType) for defining your pacts.

npm install zod    # or valibot, arktype, etc.

Core concepts

createRouter(pact)

Creates a RouterBuilder for the given pact. Chain .use(), .useNative(), and .routes() to configure it, then pass the result to toHonoRouter.

import { createRouter, toHonoRouter } from "@routepact/hono";
import { definePact } from "@routepact/core";
import { z } from "zod";

const PostPacts = definePact({
  getById: {
    method: "get",
    path: "/posts/:id",
    response: z.object({ id: z.string(), title: z.string() }),
  },
  create: {
    method: "post",
    path: "/posts",
    request: z.object({ title: z.string() }),
    response: z.object({ id: z.string(), title: z.string() }),
  },
});

const built = createRouter(PostPacts).routes({
  getById: (route) =>
    route.handler(({ params }) => ({
      status: 200,
      body: { id: params.id, title: "Hello" },
    })),
  create: (route) =>
    route.handler(({ body }) => ({
      status: 201,
      body: { id: "1", title: body.title },
    })),
});

export default toHonoRouter(built);

toHonoRouter(built)

Converts a BuiltRouter into a Hono app instance. Throws "API Router Setup Failed" if any handler is missing or a duplicate route is detected.

Handler return value

Return { status, body } from the handler. When a response schema is defined on the pact, body is validated before sending. Return { status: 204, body: undefined } to send an empty response.

route.handler(() => ({ status: 200, body: { id: "1", title: "Hello" } }))
route.handler(() => ({ status: 204, body: undefined }))

Server-Sent Events (SSE)

Mark a pact route with sse: true to create a streaming endpoint. The handler receives a sendEvent function instead of returning { status, body } — call it one or more times to push typed events to the client. A discriminated union is the natural response schema for SSE, letting you send different event shapes in a single stream:

const EventPacts = definePact({
  stream: {
    method: "get",
    path: "/events/:roomId",
    sse: true,
    response: z.discriminatedUnion("type", [
      z.object({ type: z.literal("message"), text: z.string() }),
      z.object({ type: z.literal("ping"), timestamp: z.number() }),
    ]),
  },
});

const built = createRouter(EventPacts).routes({
  stream: (route) =>
    route.handler(async ({ params, sendEvent }) => {
      await sendEvent({ type: "message", text: `Hello from ${params.roomId}` });
      await sendEvent({ type: "ping", timestamp: Date.now() });
      // handler returns void — stream closes when the function resolves
    }),
});
  • Each sendEvent call validates the data against the response schema and writes a data: ...\n\n SSE frame
  • Content-Type: text/event-stream is set automatically
  • Middleware (router-level and route-level) runs normally before the handler
  • The handler return type is void — do not return { status, body } for SSE routes
  • The connection closes when the handler function returns. For a long-lived stream, keep the handler alive with a loop. Use c.req.raw.signal to detect client disconnect:
route.handler(async ({ sendEvent, c }) => {
  const signal = c.req.raw.signal;

  while (!signal.aborted) {
    await sendEvent({ type: "ping", timestamp: Date.now() });
    await new Promise(r => setTimeout(r, 30_000));
  }
})

### Handler context

| Property     | Type                          | Description                                                                        |
| ------------ | ----------------------------- | ---------------------------------------------------------------------------------- |
| `params`     | inferred from path string     | Path parameters (e.g. `{ id: string }` for `/posts/:id`). `{}` if no params.      |
| `query`      | inferred from `query` schema  | Validated query parameters. `{}` if the pact has no `query` schema.               |
| `body`       | inferred from `request` schema | Parsed and validated request body. `never` if the pact has no `request` schema.  |
| `extensions` | merged middleware returns     | Typed object with all additions returned by upstream middleware. `{}` if none.     |
| `c`          | `Context`                     | Hono `Context` — use for headers, cookies, raw request/response, etc.             |

---

## Middleware

### Router-level middleware (`.use()`)

Runs for every route in the router. Can return an object to add typed properties to `extensions` in downstream middleware and handlers.

```ts
const built = createRouter(PostPacts)
  .use(({ c }) => ({ userId: c.req.header("x-user-id") ?? "" }))
  .routes({
    getById: (route) =>
      route.handler(({ extensions }) => ({
        status: 200,
        body: { id: "1", title: `Viewed by ${extensions.userId}` },
      })),
    // ...
  });

If the middleware doesn't need to add anything to extensions, return void (or nothing):

createRouter(PostPacts).use(() => {
  console.log("request received");
});

Route-level middleware (.use())

Runs only for a specific route. Same signature as router-level middleware, but has access to the route's params, query, and body in addition to extensions.

route
  .use(({ params }) => ({ capturedId: params.id }))
  .handler(({ extensions }) => ({
    status: 200,
    body: { id: extensions.capturedId, title: "Hello" },
  }))

defineMiddleware

Helper for defining reusable typed middleware. Pre-typed with the Hono framework context so you get autocomplete on c.

import { defineMiddleware } from "@routepact/hono";

// No requirements — everything inferred from the function body
const withAuth = defineMiddleware(({ c }) => {
  const userId = c.req.header("x-user-id");
  if (!userId) throw new Error("Unauthorized");
  return { userId };
});

// Use at router or route level
createRouter(PostPacts)
  .use(withAuth)
  .routes({ ... });

// Or on a specific route
route.use(withAuth).handler(({ extensions }) => {
  // extensions.userId is typed as string
});

Middleware defined with defineMiddleware can be shared across routers and routes.

Declaring requirements

Use type parameters to declare what the middleware requires and what it adds:

defineMiddleware<TAdds, TReqs>(fn)

| Parameter | Default | Description | | --- | --- | --- | | TAdds | void | Object added to extensions, or void for guards that add nothing | | TReqs | {} | Config object with optional requires (shape extensions must have) and/or params (path params that must be present) |

Guard that requires a prior extension and a specific path param:

type User = { id: string; role: string };

// Requires extensions.user (set by withAuth) and params.spaceId (from /:spaceId route)
const spaceGuard = defineMiddleware<void, { requires: { user: User }; params: { spaceId: string } }>(
  ({ extensions, params }) => {
    if (!canAccess(extensions.user, params.spaceId)) {
      throw new Error("Forbidden");
    }
  }
);

createRouter(SpacePacts).routes({
  getSpace: (route) =>
    route
      .use(withAuth)    // adds extensions.user
      .use(spaceGuard)  // TypeScript enforces: user in extensions ✓, :spaceId in path ✓
      .handler(...)
});

Middleware that adds to extensions and requires a prior extension:

// Adds extensions.role, but requires extensions.userId to already be present
const withRole = defineMiddleware<{ role: string }, { requires: { userId: string } }>(
  ({ extensions }) => ({ role: getRole(extensions.userId) })
);

TypeScript enforces requirements at the call site — using a middleware with unmet requirements is a compile error.

Native Hono middleware (.useNative())

Use standard Hono MiddlewareHandler functions directly. Native middleware runs in registration order alongside internal middleware and supports onion-style execution (code after await next() runs after the handler).

import { compress } from "hono/compress";

// Router level
createRouter(PostPacts)
  .useNative(compress())
  .routes({ ... });

// Route level — with onion execution
route
  .useNative(async (c, next) => {
    console.log("before handler");
    await next();
    console.log("after handler");
  })
  .handler(() => ({ status: 200, body: { id: "1", title: "Hello" } }))

Middleware execution order

Middleware runs in registration order. Router-level middleware (both internal and native) always runs before route-level middleware. Native middleware supports onion-style execution.

createRouter(PostPacts)
  .useNative(async (_c, next) => { calls.push("router-native"); await next(); })
  .routes({
    getById: (route) =>
      route
        .use(() => calls.push("route-internal"))
        .useNative(async (_c, next) => { calls.push("route-native"); await next(); })
        .handler(() => { calls.push("handler"); return { status: 200, body: { id: "1", title: "Hi" } }; }),
  });

// Order: router-native -> route-internal -> route-native -> handler

Controller pattern

HonoHandlerContext is a convenience type that combines the route's inferred types with the Hono framework context. Use it to type handler methods defined outside the inline builder chain — e.g. in a controller class.

import { createRouter, toHonoRouter, HonoHandlerContext } from "@routepact/hono";

type AuthExtensions = { userId: string };

class PostController {
  getById(ctx: HonoHandlerContext<typeof PostPacts["getById"], AuthExtensions>) {
    return { status: 200, body: { id: ctx.params.id, title: "Hello" } };
  }
}

const controller = new PostController();

const built = createRouter(PostPacts)
  .use(({ c }): AuthExtensions => ({ userId: c.req.header("x-user-id") ?? "" }))
  .routes({
    getById: (route) => route.handler(controller.getById.bind(controller)),
    // ...
  });

Note: When passing a class method as a handler, this must be bound explicitly — otherwise it will be undefined at call time. Use .bind(controller) or wrap it in an arrow function:

// bind
route.handler(controller.getById.bind(controller))
// arrow wrapper
route.handler((ctx) => controller.getById(ctx))

The second type parameter (`TExtensions`) defaults to `Record<never, never>` and can be omitted when no middleware adds extensions.

---

## Validation

Request and response validation is applied automatically when a pact has the corresponding schemas.

**Request validation** — if the pact has a `request` schema (for `POST`/`PATCH`/`PUT`) or a `query` schema, the incoming data is validated before the handler runs. On failure, a `RequestValidationError` (status 400) is thrown.

**Response validation** — if the pact has a `response` schema, the return value of the handler is validated before it is sent. On failure, a `ResponseValidationError` (status 500) is thrown.

Register an error handler on the Hono app to format validation errors:

```ts
import { RequestValidationError, ResponseValidationError, ValidationError } from "@routepact/hono";

const app = toHonoRouter(built);

app.onError((error, c) => {
  if (error instanceof RequestValidationError) {
    return c.json({ message: "Invalid request", errors: err.cause }, 400);
  }
  if (error instanceof ResponseValidationError) {
    console.error("Response validation failed:", err.cause);
    return c.json({ message: "Internal server error" }, 500);
  }
  // Or catch any validation error:
  if (error instanceof ValidationError) {
    return c.json({ message: err.message }, err.status as any);
  }
  return c.json({ message: "Internal server error" }, 500);
});

Duplicate route detection

toHonoRouter throws at startup if two routes in a pact share the same HTTP method and path. Same path with different methods is allowed.


Type reference

| Export | Description | | ------------------------- | ------------------------------------------------------------------------- | | createRouter(pact) | Creates a RouterBuilder typed for Hono | | toHonoRouter(built) | Converts a BuiltRouter into a Hono app instance | | defineMiddleware<TAdds, TReqs>(fn) | Creates a reusable typed middleware with Hono framework context; TReqs is a config object with optional requires and params fields | | ValidationError | Base class — has status and cause: StandardSchemaV1.Issue[] | | RequestValidationError | Thrown on invalid request body or query (400) | | ResponseValidationError | Thrown on invalid response body (500) | | HonoFrameworkContext | { c: Context } — the Hono context passed to all middleware and handlers | | HonoHandlerContext<TRoute, TExtensions> | Handler context type for a given pact route and middleware extensions — use to type controller methods |