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

@pronghorn/openapi

v0.1.0

Published

OpenAPI 3.1 document generation and Swagger UI for Pronghorn, built directly from your routes' Zod schemas via z.toJSONSchema.

Readme

OpenAPI 📘

OpenAPI is a lightweight, TypeScript-first documentation plugin built as an external extension for Pronghorn. It generates a live OpenAPI 3.1 specification and a Swagger UI page directly from your routes' existing Zod schemas, with zero duplicated schema definitions and zero extra dependencies.

Built as a standalone package (@pronghorn/openapi), reads app.getRoutes() and converts each route's schema.body/schema.query via Zod 4's native z.toJSONSchema(), no zod-to-json-schema third-party dependency required.

Why OpenAPI

Fastify has strong auto-generated API docs via @fastify/swagger, reading directly from a route's schema option. Pronghorn's routes already declare Zod schemas for validation via the schema option, but nothing turned that into browsable documentation. This plugin closes that gap without asking you to define your API shape twice.

  • Generates a full OpenAPI 3.1 document from routes you've already registered, no separate spec file to maintain.
  • Converts Zod schemas to JSON Schema using Zod 4's built-in z.toJSONSchema(), keeping validation and documentation perfectly in sync by construction.
  • Serves a ready-to-use Swagger UI page at a configurable path, no separate static asset bundling needed.
  • Path parameters (:id segments) are automatically included as OpenAPI path parameters.
  • Regenerates the spec on every request to the JSON endpoint, so newly registered routes always show up without a rebuild step.
  • Zero runtime dependencies beyond zod (already a Pronghorn dependency).

Installation

bun add @pronghorn/openapi

Requires Bun >=1.3.0, pronghorn >=0.1.2 as a peer dependency, and Zod 4 (Pronghorn's own dependency) for z.toJSONSchema() support.

Quick Start

import { createApp } from 'pronghorn'
import { openApiPlugin } from '@pronghorn/openapi'
import { z } from 'zod'

const app = createApp()

app.get('/users/:id', context => context.json({ id: context.params.id }), {
  schema: { query: z.object({ include: z.string().optional() }) }
})

app.post('/users', context => context.json({ created: true }), {
  schema: { body: z.object({ name: z.string(), email: z.string().email() }) }
})

// Register last, after all routes are defined
await app.register(openApiPlugin, { title: 'My API', version: '1.0.0' })

await app.listen(4000)
  • GET /openapi.json -> the generated OpenAPI 3.1 spec.
  • GET /docs -> an interactive Swagger UI browsing that spec.

Register openApiPlugin after all your routes are defined, it reads app.getRoutes() lazily on each request to /openapi.json, so routes registered afterward are still picked up, but registering the plugin itself before any routes exist would only mean an empty spec until the first request arrives, not a hard requirement.

Core Concepts

Path parameters

Any :param segment in a registered route is automatically converted into an OpenAPI path parameter, typed as a string, no extra configuration needed.

app.get('/orders/:orderId/items/:itemId', handler)
// -> documented as /orders/{orderId}/items/{itemId} with two required path parameters

Query parameters

Declared via a route's schema.query, each top-level Zod field becomes an individual OpenAPI query parameter, with required reflecting whether the field is optional in the schema.

app.get('/search', handler, {
  schema: {
    query: z.object({
      q: z.string().min(1),
      page: z.coerce.number().default(1),
      tags: z.array(z.string()).optional()
    })
  }
})

Request bodies

Declared via a route's schema.body, converted into an OpenAPI requestBody with a application/json media type and the full JSON Schema representation of your Zod object.

app.post('/orders', handler, {
  schema: {
    body: z.object({
      productId: z.string(),
      quantity: z.number().int().positive()
    })
  }
})

Custom metadata

Pass title, version, description, and servers to populate the spec's info and servers sections, shown at the top of the Swagger UI page.

await app.register(openApiPlugin, {
  title: 'Storefront API',
  version: '2.1.0',
  description: 'Public API for the storefront checkout flow',
  servers: [
    { url: 'https://api.example.com', description: 'Production' },
    { url: 'http://localhost:4000', description: 'Local development' }
  ]
})

Custom paths

Change where the spec and UI are served if /openapi.json//docs conflict with existing routes.

await app.register(openApiPlugin, {
  jsonPath: '/api/spec.json',
  docsPath: '/api/docs'
})

Plugin Options

| Option | Type | Default | Description | | ------------- | ----------------------------------------- | ----------------- | ---------------------------------------------------------- | | title | string | 'API' | Spec title shown in info.title and the Swagger UI header | | version | string | '1.0.0' | API version shown in info.version | | description | string | - | Optional longer description shown in info.description | | jsonPath | string | '/openapi.json' | Path serving the raw OpenAPI JSON document | | docsPath | string | '/docs' | Path serving the Swagger UI HTML page | | servers | { url: string; description?: string }[] | [] | Server base URLs listed in the spec |

API Reference

openApiPlugin: PluginFn - register via app.register(openApiPlugin, options).

Lower-level document generation is also exported for advanced use, e.g. writing the spec to a static file at build time instead of serving it dynamically.

import { buildOpenApiDocument } from '@pronghorn/openapi'

const document = buildOpenApiDocument(app.getRoutes(), { title: 'My API', version: '1.0.0' })
await Bun.write('openapi.json', JSON.stringify(document, null, 2))

Architecture

OpenAPI is split into two modules, each with a single responsibility.

| Module | Responsibility | | ------------- | -------------------------------------------------------------------------------------------------------------------------------- | | document.ts | Walks registered routes, converts path segments and Zod schemas into an OpenAPI 3.1 paths object via z.toJSONSchema() | | plugin.ts | Registers the /openapi.json and /docs routes on the app, serving the generated document and a Swagger UI shell that loads it |

The Swagger UI page itself loads swagger-ui-dist from a CDN (unpkg.com) rather than bundling static assets into the package, keeping the plugin's own footprint minimal. If you need fully offline docs, host swagger-ui-dist yourself and point docsPath at your own HTML instead.

Limitations

  • Only schema.body and schema.query are documented for now.
  • Response schemas are not inferred meaning every documented endpoint currently shows generic 200/400 descriptions without a response body shape yet.
  • Query parameter nesting (objects within objects) is flattened to top-level parameters only, deeply nested query schemas may not render as expected in Swagger UI.

License

WTFPL (Do What the Fuck You Want to Public License), see LICENSE for details.