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.
Maintainers
Readme
ts-route-openapi
Generate an OpenAPI 3.0.3 spec from a TypeScript route→controller codebase by statically analyzing the source with ts-morph — no runtime instrumentation, decorators, JSDoc, or annotations required.

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 yamlopenapi: 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-openapiCLI 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:
- Registration call-sites — any call of the shape
something.<verb>('/path', handler)where<verb>is one ofget,post,put,patch,deleteand 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. - NestJS decorators —
@Controller('base')classes with@Get/@Post/@Put/@Patch/@Deletemethods. - tRPC routers —
router({...})/t.router({...})procedure maps, including nested sub-routers. Each.query()/.mutation()procedure is exposed as a syntheticGET/POST <base>/<dotted.path>route (default base/trpc, configurable viagenerate()'strpc.basePathoption).
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:
- Path params — any parameter whose name matches a
:tokenin the route path (e.g.idin/users/:id) is classified as a path parameter. - Body — the first remaining parameter that is object-typed (and not an array) is classified as the request body.
- 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 fromrequired. - String-literal unions (
'a' | 'b') map toenum; numeric-literal unions to a numberenum; other unions map tooneOf(discriminated object unions becomeoneOfover$refs).null/undefinedmembers are stripped. Datemaps 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) => voidbecomes{ description: 'Function: (id: string) => void' }. A property's own JSDoc (when--descriptionsis enabled) overrides the signature text. - Named
interface/class/typealias declarations from your project are hoisted intocomponents.schemasand referenced via$ref(recursive types self-reference); library/node_modulestypes 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
--descriptionsis enabled, handler JSDoc adds operationsummary/description,@deprecatedmarks operations deprecated, and property JSDoc becomes schema propertydescription.
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 whenXis a NestJS built-inHttpExceptionsubclass (by name), a genericHttpException(_, status), or a local class whose own or inheritedstatus/statusCodeproperty is a numeric literal. Express'sapp.use((err, req, res, next) => ...)error-handling middleware contributes itsres.status(N)calls to every route registered on the sameapp/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|.
