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

typespec-hono

v0.31.1

Published

TypeSpec emitter: generate a Hono server, and the Zod validators it enforces, from an HTTP service definition, agreeing with the OpenAPI document @typespec/openapi3 publishes from the same source.

Readme

typespec-hono

Generate a Hono server from a TypeSpec API definition. Routing, request validation and handler types all come from the spec.

Add @typespec/openapi3 to the same config and it writes the OpenAPI document from that same definition, so your server and your docs cannot drift apart.

Validation and types come from typespec-http-zod, which this runs for you, so your config lists one emitter.

Install

pnpm add -D typespec-hono @typespec/compiler @typespec/http @typespec/openapi
pnpm add hono @hono/zod-validator zod secure-json-parse

Install the three @typespec packages at one version, 1.15 or later. Peer dependencies: hono, @hono/zod-validator, zod, secure-json-parse, @typespec/compiler, @typespec/http and @typespec/openapi.

# tspconfig.yaml
emit:
  - typespec-hono
options:
  typespec-hono:
    emitter-output-dir: "{project-root}/src/generated"
    seal-object-schemas: true

Quick start

You write four files. Everything under src/generated/ is produced by the compiler and never edited:

main.tsp              your API definition
tspconfig.yaml        which emitters to run
src/
  generated/          written by `tsp compile`, never edited by hand
    app.gen.ts
    runtime.gen.ts
    schemas.gen.ts
  deps.ts             your application's answers
  index.ts            your handlers, and the app

main.tsp

import "@typespec/http";
using Http;

@service(#{ title: "Widgets" })
namespace Widgets;

model Widget {
  id: string;
  name: string;
}

@error
model NotFound {
  @statusCode statusCode: 404;
  message: string;
}

@route("/widgets")
interface WidgetRoutes {
  @get list(@query limit?: int32): Widget[];
  @get read(@path id: string): Widget | NotFound;
}
pnpm exec tsp compile .

src/index.ts

A handler returns { status, body } for any response its operation declares, the failures included. input and every declared response are known from the spec, so a handler returning a status the spec does not declare, or the wrong body for a status, does not compile:

import { Hono } from "hono";
import { registerRoutes, type Operations } from "./generated/app.gen.js";
import { deps, type Caller } from "./deps.js";

const handlers = {
	WidgetRoutes_list: (ctx, input) => ({ status: 200, body: widgets.slice(0, input.limit ?? 20) }),
	WidgetRoutes_read: (ctx, input) => {
		const widget = widgets.find((w) => w.id === input.id);
		return widget === undefined
			? { status: 404, body: { message: `no widget ${input.id}` } }
			: { status: 200, body: widget };
	},
} satisfies Operations<Caller>;

export default registerRoutes(new Hono(), () => handlers, deps);

satisfies keeps each status a literal, which is what selects the response it belongs to. Without it, status: 404 widens to number, which no declared response admits.

The generated route serves each response with the Hono call for it - c.json(body, 404) for the failure above - after checking the body against the schema the document publishes for that status. So Hono's RPC client narrows a response body by its status, and a body the document does not permit reaches app.onError as a ResponseContractError rather than a caller.

Leave the factory passed to registerRoutes unannotated: annotating it widens the value and disables the check that catches a handler for an operation the spec no longer declares. It is a factory rather than an object because a Workers service binding lives on c.env and exists only for the duration of a request.

src/deps.ts

Five hooks, each answering something the spec does not contain:

| hook | the spec says | you say | | --------------- | ---------------------------------------------- | ------------------------------------ | | authorize | which schemes and scopes an operation needs | whether this caller satisfies them | | context | whether a caller is none, optional or required | who the caller is | | noContext | | what to answer when there is not one | | notAcceptable | which media types are offered | what to answer when none match | | invalid | the schema | what a validation failure looks like |

import type { AppEnv, RouteDeps } from "./generated/runtime.gen.js";

export interface Caller {
	readonly userId: string;
}

export const deps: RouteDeps<AppEnv, Caller> = {
	// The requirements are the document's, verbatim: `[{ BearerAuth: [] }]`. Satisfying any ONE of
	// them authorises; every scheme within one must be satisfied together. Waving a scheme through
	// because you do not check it is how a gated route ends up ungated.
	authorize: (requirements) => async (c, next) => {
		const bearer = /^Bearer\s+(\S+)$/i.exec(c.req.header("authorization") ?? "")?.[1];
		const satisfied = requirements.some((requirement) =>
			Object.entries(requirement).every(
				([scheme, scopes]) =>
					scheme === "BearerAuth" && bearer !== undefined && scopes.length === 0,
			),
		);
		if (!satisfied) return c.json({ error: "unauthorized" }, 401);
		await next();
		return undefined;
	},
	context: (c) => {
		const userId = c.req.header("x-user");
		return userId === undefined ? null : { userId };
	},
	noContext: (c) => c.json({ error: "unauthorized" }, 401),
	notAcceptable: (c, offered) => c.json({ error: "not_acceptable", offered }, 406),
	invalid: (result, c) => (result.success ? undefined : c.json({ error: "invalid" }, 400)),
};

authorize is the gate the document publishes, and an empty one is an open door. A requirement naming a scheme you do not check must not be satisfied: [{}], which @useAuth(NoAuth | X) publishes for its anonymous alternative, is the only requirement every caller satisfies, and it is satisfied by Object.entries({}).every(...) being vacuously true rather than by a special case.

context is told "none" (the operation needs nobody), "optional" (anonymous access is one alternative, so read a credential if one was presented) or "required". Whatever it returns is what every handler receives as ctx. Your Hono environment - bindings and variables - is an augmentation of the emitted AppEnv:

declare module "./generated/runtime.gen.js" {
	interface AppEnv {
		Bindings: { DB: D1Database };
		Variables: { requestId: string };
	}
}

Routing, request validation, the handler types and how each declared response is served come from the spec. What is left is the four files above.

What it emits

Into your output directory:

| file | what it is | | ---------------------- | ---------------------------------------------------------------------------------------------------------- | | app.gen.ts | the server: routes, validators, what each operation may answer, and the handlers | | runtime.gen.ts | the types your deps implements against, and the helpers the server calls | | schemas.gen.ts | a Zod schema for every request and response, and the status arms each declares | | vocabularies.gen.ts | shared enums, where the spec declares them | | requests.gen.ts | the contract types: every operation's input, success bodies and result, when contracts-output-dir is set | | wire-contract.gen.ts | assertions that the schemas and the types agree, with the same option |

The last four are typespec-http-zod's; see its README for what they contain.

runtime.gen.ts is written on every compile. Your own code imports from it and augments AppEnv in it, and nothing replaces it.

Routes are grouped into a sub-app per resource and mounted with app.route(), following Hono's best-practices guide. Handlers are written directly after the path definitions rather than lifted into separate controller files, because a handler in another file cannot infer its path parameters. A resource with a single route gets no sub-app.

The output is plain Hono and @hono/zod-validator, not @hono/zod-openapi. That package generates a document from the code, which would compete with the one openapi3 publishes from the spec. The responses a route serves are the ones @hono/zod-openapi would require of it: across the conformance corpus, what Hono's RPC client infers for 627 routes is compared with what RouteConfigToTypedResponse derives from the published document.

Docs

  • Guides: declared failures, middleware, the RPC client, authentication, base paths, HEAD operations, request bodies, streaming, observability, upgrading
  • Cloudflare Workers: which router to pick, and what the bundle costs
  • Reference: every option, every diagnostic, and the known limits
  • Releasing: rehearsing a two-package release against a local registry, because the server resolves the library through npm and CI cannot verify a change spanning both

Licence

MIT