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

@nxgt/openapi-hono

v0.3.0

Published

Readme

@nxgt/openapi-hono

Typed Hono routes for an OpenAPI spec. This is the runtime that the hono.ts file generated by @nxgt/openapi-codegen binds to its spec:

  • each route validates its request with the generated validators before the handler runs;
  • a request the spec refuses gets a 400 that lists every issue;
  • a reply the spec does not declare does not compile.

It replaces @nxgt/openapi-codegen/hono, the subpath that held this runtime in codegen 0.1.0. Code generated by codegen 0.2.0 and later imports @nxgt/openapi-hono instead.

Install

bun add @nxgt/openapi-hono hono zod
bun add -d @nxgt/openapi-codegen typescript
  • hono is a required peer. This package imports it for types only; your app imports it to build the app.
  • typescript 6 is a required peer, as for every @nxgt package.
  • zod is not a peer, but the app needs it: the generated operations.ts imports it.
  • The generator is only a dev dependency.

Setup

Generate with the hono option:

// openapi-codegen.config.ts
import { defineConfig } from '@nxgt/openapi-codegen';

export default defineConfig({
	input: 'openapi/openapi.yaml',
	output: 'src/generated',
	hono: true,
});

bunx nxgt-openapi generate then writes hono.ts beside the other generated files. Import createRoutes and createApi from that file, not from this package: there they are bound to your spec.

Usage

Registering routes

import { Hono } from 'hono';
import { createRoutes } from './generated/hono';

const app = new Hono();
const routes = createRoutes(app);

routes.put('/employees/{id}', auth, async (c) => {
	const { id } = c.req.valid('param');
	const employee = await employees.update(id, c.req.valid('json'));
	if (!employee) return c.json({ message: 'errors.not-found' }, 404);
	return c.json(employee, 200);
});

routes.operation('deleteEmployee', auth, async (c) => {
	await employees.remove(c.req.valid('param').id);
	return c.body(null, 204);
});
  • Paths are written as the spec writes them: {id}, not :id. Each method offers only the paths that have an operation for it.
  • c.req.valid() holds param, query, header, and json or form for a body, already validated. A text body is validated too; read it with c.req.text().
  • A route runs as [...middlewares, validator, handler], so a 401 from auth comes before a 400.
  • routes.validate marks where validation runs, for a middleware that needs validated input: routes.put(path, auth, routes.validate, owns, handler).

Validation errors

By default, a request the spec refuses gets a 400 response. Its body is { status, message: 'errors.validation-failed', timestamp, issues }, and each issue is { target, path, code, message }. @nxgt/openapi-codegen declares it, as ValidationErrorBody, on every operation that takes an input, so a client that checks its replies reads it. To answer differently, generate with validationErrors: false and declare your own body:

const routes = createRoutes(app, {
	onValidationError: (failure, c) => {
		throw new CustomException(400, 'errors.validation-failed', {
			issues: failure.issues,
		});
	},
});

The hook can:

  • return a Response, which is sent;
  • throw, which hands the failure to app.onError;
  • return nothing, which sends the default.

routes.with({ onValidationError }) sets the hook for the routes registered through it only.

Checking replies

const routes = createRoutes(app, {
	validateResponses: process.env.NODE_ENV !== 'production',
});

Every reply is checked against the spec: its status, its Content-Type, and, for JSON and text, its body. A reply that fails goes through onValidationError, as a 500 by default, its issues logged with console.error rather than sent. The check reads every reply body twice, so keep it for development and tests.

Streams

An operation whose reply the spec describes an item at a time, with OpenAPI 3.2's itemSchema, streams it from its handler with the hono.ts helpers:

import { createRoutes, streamEvents, streamLines } from './generated/hono';

createRoutes(app)
	.get('/feed', (c) =>
		streamEvents(c, 'watchFeed', async (stream) => {
			await stream.write({ event: 'update', id: '7', data: item }); // Item, as JSON
			await stream.write({ event: 'ping', data: 'still here' }); // text
			while (!stream.aborted) await stream.sleep(15_000);
		}),
	)
	.post('/export', (c) =>
		streamLines(c, 'exportItems', async (stream) => {
			for (const item of items) await stream.write(item);
		}),
	);
  • Each event is typed by its name, and its data is sent as JSON when the spec declares it JSON, as text otherwise. JSON lines go out as the media type the spec declares.
  • With validateResponses, each item is checked before it is sent. The status is out already, so a failing item goes to onValidationError for its side effects, is not sent, and ends the stream.
  • stream.aborted and stream.onAbort() tell when the client went away.

Modules

import { createApi } from './generated/hono';

export const api = createApi();

// employees.routes.ts
const employees = new Hono();
api
	.routes(employees, { prefix: '/employees', tag: 'employees' })
	.get('/employees', listEmployees)
	.get('/employees/{id}', getEmployee);

// app.ts
app.route('/employees', employees);
api.assertComplete(); // throws, listing every operation without a route
  • prefix is where the sub-app is mounted. Only paths under it are offered.
  • tag offers only the operations that carry that tag.
  • api.missing(tag?) lists the operations that have no route.
  • api.assertComplete(tag?) throws when any operation has no route.

createRoutes(app, options) is createApi(options).routes(app, options): a registry for that one app.

Registering a route throws at startup in these cases:

  • the spec has no operation at that method and path, or no such operationId;
  • the operation already has a route;
  • an earlier route would always answer first;
  • the operation is outside the prefix or the tag;
  • the last argument is not the handler, or routes.validate appears twice;
  • Hono cannot route it: a HEAD operation (Hono answers it with the GET route), or a path parameter that does not fill its segment (/files/{name}.json). Generating warns about both, and missing() leaves them out.

API

The app imports from its generated hono.ts, which binds this package to the spec. The package's own exports are what hono.ts is built from, and the types of a failure for your own onValidationError.

Generated hono.ts

Generated with hono: true. S below is its HonoSpec.

createApi()

const createApi: (options?: ApiOptions) => Api<HonoSpec>;

One registry for the whole spec: api.routes(app) in each module, then api.assertComplete(). options are the defaults of every routes() it makes (ApiOptions). See Modules.

createRoutes()

const createRoutes: <Prefix extends string = '', Tag extends keyof OperationsByTag & string = never>(
	app: Hono<any, any, any>,
	options?: RoutesOptions<Prefix, Tag>,
) => Routes<HonoSpec, ScopeOf<HonoSpec, Tag>, Prefix>;

Routes on one app, with a registry of their own: createApi(options).routes(app, options). Takes RoutesOptions and returns Routes. See Registering routes.

streamEvents()

const streamEvents: <Id extends /* each operation that replies with server-sent events */>(
	c: Context<any, any, any>,
	id: Id,
	write: (stream: EventWriter<Operations[Id]['stream']['item']>) => Promise<void>,
) => Replies[Id];

Replies with the operation's server-sent events, each typed by the spec. Generated only when the spec has such an operation. Behaves as the package's streamEvents(). See Streams.

streamLines()

const streamLines: <Id extends /* each operation that replies with JSON Lines */>(
	c: Context<any, any, any>,
	id: Id,
	write: (stream: LineWriter<Operations[Id]['stream']['item']>) => Promise<void>,
) => Replies[Id];

Replies with the operation's JSON lines, each typed by the spec. Generated only when the spec has such an operation. Behaves as the package's streamLines().

Replies

interface Replies {
	updateEmployee:
		| TypedResponse<Employee, 200, 'json'>
		| TypedResponse<Problem, 404, 'json'>;
	deleteEmployee: TypedResponse<null, 204, 'body'>;
}

What each operation may reply, keyed by operationId: a handler returning anything else does not compile. A JSON body is typed as JSON carries it, a reply without content as c.body(null, status) (and c.redirect() for a 3xx other than 304), a binary or streamed body as unknown. A status Hono has no type for is typed any, and an operation that declares only default or ranges replies Response.

HonoSpec

interface HonoSpec {
	operations: Operations;
	replies: Replies;
	routes: OperationsByRoute;
	paths: PathsByMethod;
	tags: OperationsByTag;
	tagPaths: PathsByTag;
}

The spec, as this package reads it: the ApiSpec that every generic below is given. The indexes come from the generated types.ts.

Functions

createApi()

function createApi<S extends ApiSpec>(
	operations: OperationTable,
	defaults?: ApiOptions,
): Api<S>;

The engine: one registry over the operations table of operations.ts. defaults apply to every routes() it makes, under the options given there. Returns an Api. hono.ts calls it; an app calls the generated createApi() instead.

validationErrorHandler()

const validationErrorHandler: (failure: ValidationFailure, c: Context) => Response;

The default answer to a failure. For a request, a 400: { status: 400, message: 'errors.validation-failed', timestamp, issues }. For a response, a 500 without the issues, which would show what the reply held: { status: 500, message: 'errors.response-validation-failed', timestamp }, the issues going to console.error. Call it from your own hook to fall back to it.

streamEvents()

function streamEvents<Event>(
	c: Context,
	id: string,
	write: (stream: EventWriter<Event>) => Promise<void>,
): Response;

Replies with server-sent events from the handler of operation id, as text/event-stream with cache-control: no-cache, under the operation's first 2xx status that declares them. The reply ends when write returns; a throw inside it ends it too and goes to console.error. Throws when id is not the route running c, or when the operation does not reply with server-sent events. See Streams.

streamLines()

function streamLines<Item>(
	c: Context,
	id: string,
	write: (stream: LineWriter<Item>) => Promise<void>,
): Response;

Replies with JSON lines from the handler of operation id: one JSON text a line, as the media type the spec declares (application/jsonl, application/x-ndjson, or application/json-seq, which puts a record separator before each). Ends and throws as streamEvents().

Objects

Api

interface Api<S extends ApiSpec> {
	routes(app, options?): Routes;
	missing(tag?): string[];
	assertComplete(tag?): void;
}

What createApi() returns: one registry, shared by every routes() it makes. See Modules.

api.routes()
routes<Prefix extends string = '', Tag extends keyof S['tags'] & string = never>(
	app: Hono<any, any, any>,
	options?: RoutesOptions<Prefix, Tag>,
): Routes<S, ScopeOf<S, Tag>, Prefix>;

Registers routes on app, which may be a module's sub-app. The options override the defaults given to createApi().

| Option | Type | Default | Description | | --- | --- | --- | --- | | prefix | string | none | Where app is mounted, as the spec writes it: /employees. Routes are registered relative to it, and only paths under it are offered. | | tag | a tag of the spec | none | Offers only the operations with this tag. | | onValidationError | ValidationErrorHook | validationErrorHandler | Answers a failure. | | validateResponses | boolean | false | Checks every reply against the spec. |

api.missing()
missing(tag?: keyof S['tags'] & string): string[];

The operationIds with no route yet, of one tag or of the whole spec. An operation Hono cannot route is never missing.

api.assertComplete()
assertComplete(tag?: keyof S['tags'] & string): void;

Throws an Error listing every operation of missing(tag), as operationId (METHOD /path); returns when there is none. Call it once every module is registered.

Routes

type Routes<S extends ApiSpec, Sc extends Scope = Whole<S>, Prefix extends string = ''>;

What createRoutes() and api.routes() return: a method per HTTP method, operation, validate and with. Every registration returns the same Routes, so calls chain. See Registering routes.

routes.get(), routes.put(), routes.post(), routes.delete(), routes.options(), routes.head(), routes.patch(), routes.trace(), routes.query()
get<P extends /* a GET path of the scope, starting with the prefix */>(
	path: P,
	...chain: [...MiddlewareHandler[], RouteHandler<S, /* the operationId of GET P */>]
): Routes<S, Sc, Prefix>;

Registers the operation at path, written as the spec writes it (/employees/{id}), with middlewares then its handler. Its type, Register, has one overload per chain length, up to five middlewares, then one for longer chains, as Hono's own app.get has. With a fixed length, the handler's c stays typed while its reply is being written, so the editor can offer the body's fields. The handler's c.req.valid() and replies are those of the operation. Throws at registration in the cases listed under Modules; head() always throws, since Hono answers HEAD with the GET route.

routes.operation()
operation<Id extends Sc['ids']>(id: Id, ...chain: Chain<S, Id>): Routes<S, Sc, Prefix>;

Registers an operation by its operationId instead of its path. Throws as the methods above. Its type, RegisterOperation, has the same overloads.

routes.validate
readonly validate: MiddlewareHandler;

A marker for where the request is validated in a chain, when a middleware needs validated input: routes.put(path, auth, routes.validate, owns, handler). Without it, validation runs after every middleware. It throws if it is ever run outside a routes chain.

routes.with()
with(options: ApiOptions): Routes<S, Sc, Prefix>;

The same routes, on the same app and registry, with onValidationError or validateResponses changed for the routes registered through it only.

Types

ApiOptions

| Field | Type | Description | | --- | --- | --- | | onValidationError? | ValidationErrorHook | Answers a request the spec refuses, or a reply it does not declare. Default validationErrorHandler. | | validateResponses? | boolean | Checks every reply: its status, its content type and, for JSON and text, its body. For development and tests. |

Taken by createApi() and routes.with(). See Validation errors and Checking replies.

RoutesOptions

interface RoutesOptions<Prefix extends string = '', Tag extends string = never> extends ApiOptions {
	prefix?: Prefix;
	tag?: Tag;
}

Taken by createRoutes() and api.routes(); see the table under api.routes().

ValidationFailure

| Field | Type | Description | | --- | --- | --- | | kind | 'request' \| 'response' | A request the spec refuses, or, with validateResponses, a reply it does not declare. | | operationId | string | The operation. | | method | string | Its method, lowercase. | | path | string | As the spec writes it: /employees/{id}. | | status? | number | The reply's status, for a response failure. | | issues | ValidationIssue[] | Every issue, from every target at once. |

What onValidationError receives.

ValidationIssue

| Field | Type | Description | | --- | --- | --- | | target | ValidationTarget \| 'response' | Where the value was read. | | path | (string \| number)[] | Inside the target: ['items', 0, 'name'], or [] for the whole of it. | | code | string | Zod's issue code, or one of invalid_json, invalid_form, invalid_content_type, missing_body, repeated_parameter, undeclared_status. | | message | string | What is wrong. |

One entry of ValidationFailure.issues, and of the default 400's issues.

ValidationTarget

type ValidationTarget = 'param' | 'query' | 'header' | 'json' | 'form' | 'body';

Where a request value was read: a target of c.req.valid(), or body for a text body, which a handler reads with c.req.text().

ValidationErrorHook

type ValidationErrorHook = (
	failure: ValidationFailure,
	c: Context,
) => Response | undefined | Promise<Response | undefined>;

The type of onValidationError: return a Response to send it, throw to hand the failure to app.onError, or return nothing for the default.

SchemaIssue

interface SchemaIssue {
	readonly path: readonly PropertyKey[];
	readonly code: string;
	readonly message: string;
}

What the engine reads of a Zod issue, in a Validator's failed result.

EventWriter

| Field | Type | Description | | --- | --- | --- | | write(event) | (event: Event) => Promise<void> | Sends an event, { event?, data, id?, retry? } in hono.ts. Throws for an event name the spec does not declare, or an event or id holding a line break. | | sleep(ms) | (ms: number) => Promise<void> | Resolves after ms milliseconds: a pause between two items. | | aborted | boolean | Whether the client went away: stop writing then. | | onAbort(listener) | (listener: () => void \| Promise<void>) => void | Runs listener when the client goes away. |

The stream that streamEvents() hands to write.

LineWriter

| Field | Type | Description | | --- | --- | --- | | write(item) | (item: Item) => Promise<void> | Sends an item, as a line of JSON. | | sleep(ms), aborted, onAbort(listener) | | As on EventWriter. |

The stream that streamLines() hands to write.

RouteHandler

type RouteHandler<S extends ApiSpec, Id extends string> = (
	c: DeclaredJson<Reply> & Context</* … */>, // c.req.valid() holds the operation's validated input
	next: Next,
) => Reply | Promise<Reply>; // S['replies'][Id]

The handler of operation Id: the last argument of a registration. Its c is still a Context, for any helper that takes one.

DeclaredJson

interface DeclaredJson<R> {
	json<Status extends /* a JSON status R declares */, Body extends Partial</* its body */>>(
		body: Body,
		status: Status,
		headers?: Record<string, string | string[]>,
	): Response & TypedResponse<JSONParsed<Body>, Status, 'json'>;
}

The c.json() a handler gets on top of Hono's own. It types the body by the status passed after it, so the editor offers the fields of the body declared for that status, and then only those not written yet. Hono's c.json() has nothing to offer, since it takes any T. The reply is typed as Hono types it, so what the handler returns is checked against the spec as before. A body that is not even part of the declared one falls through to Hono's c.json(), and the handler's return is refused there.

Register

interface Register<S extends ApiSpec, Sc extends Scope, Prefix extends string, M extends Method>;

The type of routes.get() and the other methods: see routes.get(). RegisterOperation<S, Sc, Prefix> is routes.operation()'s.

Chain

type Chain<S extends ApiSpec, Id extends string> = [...MiddlewareHandler[], RouteHandler<S, Id>];

Middlewares, then the handler: the arguments after the path or the operationId.

Method

type Method = 'get' | 'put' | 'post' | 'delete' | 'options' | 'head' | 'patch' | 'trace' | 'query';

The HTTP methods of an operation, and the registration methods of Routes.

ApiSpec

| Field | Type | Description | | --- | --- | --- | | operations | object | Operations, from types.ts, keyed by operationId. | | replies | object | Replies, from hono.ts, keyed by operationId. | | routes | object | OperationsByRoute: 'put /employees/{id}' to its operationId. | | paths | { [M in Method]: string } | PathsByMethod. | | tags | object | OperationsByTag. | | tagPaths | object | PathsByTag. |

What a spec must provide to type routes; the generated HonoSpec is one.

Scope

interface Scope {
	ids: string;
	paths: { [M in Method]: string };
}

The operations a Routes offers: their operationIds, and their paths by method.

Whole

The scope of every operation of a spec: Whole<HonoSpec>, the default of Routes.

Tagged

The scope of the operations of one tag: Tagged<HonoSpec, 'employees'>.

ScopeOf

Whole<S> when no tag is given, else Tagged<S, Tag>; evaluated once per routes(): ScopeOf<HonoSpec, 'employees'>.

OperationTable

type OperationTable = { readonly [operationId: string]: RuntimeOperation };

The operations table that operations.ts exports, and createApi() takes.

RuntimeOperation

| Field | Type | Description | | --- | --- | --- | | method | Method | The operation's method. | | path | string | As the spec writes it: /employees/{id}. | | honoPath | string | As Hono routes it: /employees/:id. | | tags | readonly string[] | Its tags. | | parameters | readonly { name; in: 'path' \| 'query' \| 'header'; list: boolean; explode: boolean }[] | How each parameter is read. | | param, query, header | Validator | The validator of each target. | | body? | { required: boolean; content: { [mediaType]: RuntimeMedia } } | Its request body, by media type. | | responses | { [status]: { [mediaType]: RuntimeMedia } } | Its replies, by status, then media type. |

One entry of the OperationTable.

RuntimeMedia

| Field | Type | Description | | --- | --- | --- | | kind | 'json' \| 'form' \| 'text' \| 'binary' \| 'sse' \| 'jsonl' | How the content is read or written. | | schema? | Validator | Validates the whole content. | | events? | { [event]: Validator \| null } | sse: each event's data, by name: a validator for JSON, null for text. | | item? | Validator | jsonl: each item. |

One media type of a body or a reply in a RuntimeOperation.

Validator

interface Validator {
	safeParse(value: unknown):
		| { success: true; data: unknown }
		| { success: false; error: { issues: readonly SchemaIssue[] } };
}

What the engine asks of a validator: Zod's safeParse. Every validator of the table is one.

Traps

  • Give c.json() a status. Without one, Hono types the reply with any contentful status, and it matches no declared reply. Without one, the editor also has no declared body to offer.
  • With dates: 'date', completion stops at the first Date. The declared body is the one JSON carries, with dates as strings. A body that holds a Date falls through to Hono's c.json(), which still checks it, but has no fields to offer.
  • Reply with plain objects. A Mongoose document does not type as its schema; return .lean() results.
  • Read the body through c.req, never c.req.raw. A middleware that drains c.req.raw leaves the validator nothing to read.
  • Register the static path first. /users/{id} registered before /users/me would answer for it, so the second registration throws.
  • security is not enforced. Register your own auth middlewares.
  • Limit the body size yourself. Put Hono's bodyLimit first.

Documentation

  • Typed Hono routes: how a request is read, every issue code, reply checks, modules, the mistakes caught at startup, and the traps.
  • The architecture: registration, validation, the route types, what they cost, and the tests. Read it before working on the runtime.