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

ts-route-openapi

v1.1.0

Published

Generate OpenAPI 3.0.3 specs from TypeScript route→controller code via static analysis (ts-morph) — no decorators or JSDoc required.

Readme

ts-route-openapi

npm version license: MIT

Generate an OpenAPI 3.0.3 spec from a TypeScript route→controller codebase by statically analyzing the source with ts-morphno runtime instrumentation, decorators, JSDoc, or annotations required.

ts-route-openapi preview

Your route registrations and TypeScript types are the documentation:

// users.controller.ts
export interface CreateUserInput {
  name: string;
  age?: number;
}

export class UsersController {
  static getById(id: string): { id: string; name: string } { /* ... */ }
  static create(input: CreateUserInput): Promise<{ ok: boolean }> { /* ... */ }
}

// bootstrap.ts
app.get('/users/:id', UsersController.getById);
app.post('/users', UsersController.create);
npx ts-route-openapi tsconfig.json -o openapi.yaml -f yaml
openapi: 3.0.3
paths:
  /users/{id}:
    get:
      parameters:
        - name: id
          in: path
          required: true
          schema: { type: string }
      responses:
        "200":
          content:
            application/json:
              schema:
                type: object
                properties: { id: { type: string }, name: { type: string } }
                required: [id, name]
  /users:
    post:
      requestBody:
        content:
          application/json:
            schema: { $ref: "#/components/schemas/CreateUserInput" }
      # ...
components:
  schemas:
    CreateUserInput:
      type: object
      properties: { name: { type: string }, age: { type: number } }
      required: [name]

Install

npm install --save-dev ts-route-openapi

CLI usage

npx ts-route-openapi [tsconfig] -o openapi.json -f json --title "My API" --api-version 1.0.0
npx ts-route-openapi [tsconfig] -o openapi.yaml -f yaml --watch

| Flag | Default | Description | | ------------------------- | --------------- | ------------------------------------- | | [tsconfig] (positional) | tsconfig.json | Path to the project's tsconfig.json | | -o, --out <file> | openapi.json | Output file path | | -f, --format <fmt> | json | Output format: json or yaml | | --title <title> | API | Spec info.title | | --api-version <version> | 1.0.0 | Spec info.version | | --descriptions | off | Include JSDoc summaries, descriptions, deprecation, and property descriptions | | -w, --watch | off | Regenerate when project source files change |

Programmatic usage

import { generate } from 'ts-route-openapi';

const doc = generate(
  'path/to/tsconfig.json',
  { title: 'My API', version: '1.0.0' },
  { descriptions: true },
);

How it works

The tool loads your project via its tsconfig.json and discovers routes three ways:

  1. Registration call-sites — any call of the shape something.<verb>('/path', handler) where <verb> is one of get, post, put, patch, delete and the first argument is a string literal. The handler can be a controller method reference (UsersController.getById), an inline arrow function, or an identifier pointing at a function.
  2. NestJS decorators@Controller('base') classes with @Get/@Post/@Put/@Patch/@Delete methods.
  3. tRPC routersrouter({...}) / t.router({...}) procedure maps, including nested sub-routers. Each .query()/.mutation() procedure is exposed as a synthetic GET/POST <base>/<dotted.path> route (default base /trpc, configurable via generate()'s trpc.basePath option).

It then uses the TypeScript type checker on the handler to extract parameter and return types. Your registrations are the single source of truth — nothing to keep in sync.

Framework support

| Framework | Types read from | | --- | --- | | Express | Request<Params, ResBody, ReqBody, Query> and Response<T> generics | | Fastify | FastifyRequest<{ Params; Body; Querystring; Reply }> route generic + handler return type; route schema.body / schema.querystring / schema.params from Zod or JSON-schema object literals | | NestJS | @Param('x')/@Query('x')/@Body() decorated, typed method params + return type | | Hono | TypedResponse<T> from c.json(...) return types; path params from :tokens; zValidator('json' | 'query', schema) for Zod request schemas | | Koa (+ @koa/router) | paths and :token params only (ctx carries no static route types) | | tRPC | .input(zodSchema) for the request (query param for queries, body for mutations); .output(zodSchema) or the resolver's return type for the response | | Anything else | plain-typed handlers via the classification convention below; unknown framework objects fall back to :token string params |

A registered route always makes it into the spec: when no types are recognizable the tool documents the path and its :token params as strings rather than dropping the route.

Parameter classification convention (plain typed handlers)

For each handler parameter, in declaration order:

  1. Path params — any parameter whose name matches a :token in the route path (e.g. id in /users/:id) is classified as a path parameter.
  2. Body — the first remaining parameter that is object-typed (and not an array) is classified as the request body.
  3. Query — every other remaining parameter is classified as a query parameter.

Response

The handler's return type is used as the schema for the 200 response, unwrapping Promise<T> to T when present.

Schema mapping

  • Primitives (string, number, boolean), arrays, and nested objects map to their OpenAPI equivalents; optional properties (?) are omitted from required.
  • String-literal unions ('a' | 'b') map to enum; numeric-literal unions to a number enum; other unions map to oneOf (discriminated object unions become oneOf over $refs). null/undefined members are stripped.
  • Date maps to { type: string, format: date-time }.
  • Callable-typed properties (functions, methods) map to { description: 'Function: <signature text>' } instead of an empty schema — e.g. onSave: (id: string) => void becomes { description: 'Function: (id: string) => void' }. A property's own JSDoc (when --descriptions is enabled) overrides the signature text.
  • Named interface/class/type alias declarations from your project are hoisted into components.schemas and referenced via $ref (recursive types self-reference); library/node_modules types are inlined.
  • Zod validator schemas in Hono and Fastify route metadata map common builders (object, string, number, boolean, array, enum, optional, nullable, literal, and literal unions). Unsupported constructs degrade to {} with a stderr note.
  • When --descriptions is enabled, handler JSDoc adds operation summary/description, @deprecated marks operations deprecated, and property JSDoc becomes schema property description.

Security config

Add ts-route-openapi.config.json next to your tsconfig.json to emit OpenAPI security metadata:

{
  "securitySchemes": {
    "bearerAuth": { "type": "http", "scheme": "bearer" },
    "apiKeyAuth": { "type": "apiKey", "in": "header", "name": "x-api-key" }
  },
  "security": [{ "bearerAuth": [] }],
  "securityOverrides": [
    { "method": "GET", "path": "/health", "security": [] },
    { "path": "/public/**", "security": [] }
  ],
  "publicDecorator": "Public"
}

securitySchemes is copied to components.securitySchemes; security is applied to every operation unless a securityOverrides entry matches the route verb and glob path. For NestJS, @Public() on a class or method drops the default security when publicDecorator is set.

Examples

Runnable, idiomatic Express, Fastify, NestJS, Hono, Koa, tRPC, and framework-free examples — each with its generated openapi.yaml committed — live in examples/. No adapters or code changes: install, run the CLI, get the spec.

Contributing

Planning work lives in the ts-route-openapi Plan GitHub Project, not long-lived plan documents in the repository. The main branch is protected; send changes through pull requests and keep review threads resolved before merging.

See CONTRIBUTING.md for the full workflow.

Limitations

  • Status codes are detected, not exhaustive: res.status(N) / reply.code(N) / @HttpCode(N) produce per-status responses. throw new X(...) is also detected when X is a NestJS built-in HttpException subclass (by name), a generic HttpException(_, status), or a local class whose own or inherited status/statusCode property is a numeric literal. Express's app.use((err, req, res, next) => ...) error-handling middleware contributes its res.status(N) calls to every route registered on the same app/router instance, whether or not the middleware lives in the same file. Statuses set any other way — resolved dynamically, thrown from a re-exported third-party exception class, or set by middleware on a router mounted into the app (app.use('/prefix', router)) rather than the app instance itself — are not seen.
  • Callable types (functions, methods) map to a schema-less { description: 'Function: <signature text>' } (overridden by the property's own JSDoc when present) rather than being hoisted like an object type. An overloaded callable's description joins every signature with |.

License

MIT