nestjs-openapi-parser
v0.0.10
Published
CLI that parses a NestJS project with ts-morph and generates an OpenAPI document.
Readme
nestjs-openapi-parser
Generate an OpenAPI 3.x document from a NestJS project by pure static analysis of the TypeScript source. No app boot, no reflect-metadata, no runtime dependency on @nestjs/*. Just ts-morph reading your .ts files.
Ships a CLI (nestparser) and a programmatic API.
Why
@nestjs/swagger is excellent but requires booting the application and sprinkling @Api* decorators everywhere.
nestjs-openapi-parser takes a different trade-off: it reads your existing code (controllers, DTOs, entities, JSDoc) and
produces a spec without running anything. That makes it cheap to plug into CI
and ergonomic when your codebase already follows clear conventions (TypeORM entities, class-validator DTOs, JSDoc descriptions).
Features
- Zero runtime coupling — no
@nestjs/*deps, noreflect-metadata, the parser doesn't import your code. - JSDoc-driven descriptions — class, method and property comments become OpenAPI
descriptionfields. - Reachability-only schemas — only classes reached from an endpoint end up in
components.schemas; orphans don't leak. - Nest mapped types —
PartialType / PickType / OmitType / IntersectionTypeare resolved structurally. @Query() dto: DTOis expanded into individual query parameters.class-validatorconstraints —@Min/@Max/@MinLength/@IsEmail/@IsUUID/@Matches/@IsInt… becomeminimum/maximum/minLength/format/pattern…- Self-validating output — every generated document is checked against the OpenAPI 3.x JSON Schema; an invalid spec throws instead of being emitted.
- Pluggable hooks for response envelopes, security resolution, and tag conventions.
- Documentation variants via custom
@Scopetag in JSDoc — emitpublic,internal,admin… flavors of the same spec from a single source. - Tiny config surface with sane defaults.
Requirements
- Node.js >= 18
- A NestJS (or NestJS-shaped) project with a
tsconfig.json
Install
One-off via npx:
npx nestjs-openapi-parser --project ./apps/api --out openapi.jsonAs a dev dependency:
npm install --save-dev nestjs-openapi-parser
# or
yarn add -D nestjs-openapi-parser
# or
pnpm add -D nestjs-openapi-parserOr install globally:
npm install -g nestjs-openapi-parser
nestparser --helpQuick start
- Drop a config file at your project root:
// nestparser.config.ts
import { defineConfig } from 'nestjs-openapi-parser';
export default defineConfig({
openapi: {
title: 'My API', // Default to `package.json` name
version: '1.0.0', // Default to `package.json` version
servers: [{ url: 'http://localhost:3000' }],
securitySchemes: {
bearerAuth: { type: 'http', scheme: 'bearer', bearerFormat: 'JWT' },
},
},
project: {
globalPrefix: 'v1',
},
});- Run the CLI:
npx nestparser --out openapi.jsonThat's it. You should see something like:
Project root: /path/to/your/api
Config file: /path/to/your/api/nestparser.config.ts
Wrote /path/to/your/api/openapi.json (42 routes, 58 operations, 31 schemas).CLI usage
nestparser [options]
Options:
-V, --version output the version number
-p, --project <path> path to the NestJS project root (default: cwd)
-o, --out <path> path to the OpenAPI JSON output file (default: ./openapi.json)
-c, --config <path> path to the nestparser config file (default: auto-discover)
-s, --scope <list> active scopes (comma-separated; repeatable)
-h, --help display helpConfig file discovery
The CLI looks for the first file matching, inside --project:
nestparser.config.ts
nestparser.config.mts
nestparser.config.cts
nestparser.config.mjs
nestparser.config.cjs
nestparser.config.js
nestparser.config.json.ts / .mts / .cts files are loaded via tsx (registered lazily — JSON-only users don't pay for it). Use --config <path> to point at an explicit file.
Config is optional. If auto-discovery finds nothing, the parser emits a warning and falls back to a default config — openapi.title/version come from the project's package.json (name/version), with everything else on engine defaults (rootDir: src, tsConfigFilePath: tsconfig.json, no hooks). This lets you run against a vanilla NestJS project with zero setup.
Library usage
import { parseNestProject, loadConfig } from 'nestjs-openapi-parser';
import { writeFileSync } from 'node:fs';
const projectRoot = process.cwd();
const { config } = await loadConfig({ projectRoot });
const document = await parseNestProject({ projectRoot, config });
writeFileSync('openapi.json', JSON.stringify(document, null, 2));See more in Library Usage.
How to use in NestJS
Write plain NestJS — the parser reads your controllers and models as-is. No config and no extra decorators are required; JSDoc comments simply carry over as descriptions.
/** A registered user. */
export class User {
id!: string; // → { type: 'string' }, required
/** Display name. */
name!: string; // property JSDoc → schema description
email?: string; // optional (`?`) → omitted from `required`
}import { Controller, Delete, Get, Param } from '@nestjs/common';
import { User } from './user.entity';
/** Manage users. */ // class JSDoc → `Users` tag description
@Controller('users')
export class UsersController {
/** Fetch a single user by id. */ // method JSDoc → operation description
@Get(':id') // `:id` → required path param; summary "Find One"
findOne(@Param('id') id: string): Promise<User> {
// return type `User` → 200 response body + `User` schema
}
/**
* Permanently delete a user.
*
* @Scope admin // emitted only when built with --scope admin
* @Tag Admin // groups this operation under the `Admin` tag
* @Name Delete user // overrides the auto summary
*/
@Delete(':id')
remove(@Param('id') id: string): Promise<void> {}
}Run npx nestparser --out openapi.json — that's it. See What it parses for
every supported construct (DTOs, class-validator constraints, enums, @Scope, @Tag, hooks…).
Available JSDoc tags
Add these to a controller, method, or property JSDoc to enrich the output. Each must be on its own line (inline mentions are treated as description text).
| Tag | Applies to | Effect |
| --------------------------- | ------------------------------- | ----------------------------------------------------------------------------------------------- |
| @Tag <name> | Controller or method | Group the operation under <name> instead of the tag derived from the class name |
| @Name <text> | Method | Set the operation summary (otherwise the method name, humanized) |
| @Scope <name> | Controller, method, or property | Emit the item only when <name> is an active scope. Comma-separated for several (@Scope a,b) |
| @Accept <media-type> | Method | Request body (@Body()) media type — default application/json |
| @ContentType <media-type> | Method | Response media type — default application/json |
Inside any JSDoc body you can also wrap text in <scope>…</scope> fragments to show it only under matching scopes — see Configuration.
What it parses
Configuration
See Configuration Documentation
Examples
A complete fixture lives under tests/fixtures/example-app/ — a self-contained NestJS app with controllers, DTOs, entities, mapped types, scopes and the envelope hook. The corresponding generated OpenAPI documents are snapshotted at tests/__snapshots__/ (one file per scope variant).
You can browse any snapshot in a Scalar UI:
yarn snapshot:serve # interactive picker (uses prompts)
yarn snapshot:serve openapi.admin.snap.json # basename relative to tests/__snapshots__/
yarn snapshot:serve /tmp/some-spec.json # any absolute path
SCALAR_PORT=9000 yarn snapshot:serve # override the default port (8088)Limitations
- Decorators are matched by local identifier name. Aliased imports (
import { Post as HttpPost }) won't be detected — keep them un-aliased or extend via a hook. - Pipe detection in
@Paramis textual (ParseUUIDPipe/ParseIntPipe/ParseBoolPipe). Custom pipes fall back to the parameter type. - No support yet for
@nestjs/swagger's@ApiProperty(...)runtime overrides — describe properties via JSDoc instead. - Module-level filtering (e.g. emit only routes from one Nest module) is not built in — control it at the
rootDir/excludeSuffixes/@Scopelevel. - Route patterns OpenAPI can't represent — inline regex (
:id(\d+)), wildcards (*,:splat*),+/*modifiers, and more than one optional param in a route — are skipped with a warning. Optional params (:id?) are supported via path splitting.
