Build-time OpenAPI 3.0 / 3.1 generator that parses NestJS controllers and TypeScript DTOs with oxc-parser.
Project-specific settings (parameter descriptions, tag mapping, API versioning) live in an openapi-gen.config.json at the consuming project root.
Generate from every *.controller.ts under src/:
node --import tsx packages/openapi-gen/src/cli.ts \
--root packages/back \
--all-controllers \
--out specs/generated/openapi.json \
--config openapi-gen.config.jsonGenerate from a single controller:
node --import tsx packages/openapi-gen/src/cli.ts \
--root packages/back \
--controller src/editorial/controller/page/home-page.controller.ts \
--out specs/generated/home-page.openapi.json \
--config openapi-gen.config.json \
--title "Home page API"Lint the generated spec with Spectral (duplicate operationId, missing tags, path parameter mismatches, etc.):
node --import tsx packages/openapi-gen/src/cli.ts \
--root packages/back \
--all-controllers \
--out specs/generated/openapi.json \
--config openapi-gen.config.json \
--lintUse a custom ruleset (e.g. the consuming project’s .spectral.js):
--lint --lint-ruleset .spectral.jsSpectral errors fail the command (exit code 1); warnings are reported but do not fail.
Optional openapi-gen.config.json in the project root (or pass --config <path>):
{
"title": "My API",
"parameterDescriptions": {
"id": "Resource identifier"
},
"tagsBySourceFolder": {
"authentication": "auth"
},
"preferredApiVersion": "3.0",
"openApiVersion": "3.1",
"versionHeader": "X-Api-Version",
"lintOnGenerate": true,
"spectralRulesetPath": ".spectral.js"
}title— default OpenAPI info title (overridable via--title)parameterDescriptions— descriptions keyed by parameter nametagsBySourceFolder— mapssrc/<folder>/...to OpenAPI tagspreferredApiVersion— when duplicate path/method exist, prefer this Nest route versionopenApiVersion— OpenAPI spec to emit:3.0(default) or3.1versionHeader— header name added for@Version(...)routeslintOnGenerate— run Spectral after writing the spec (same as--lint)spectralRulesetPath— custom Spectral ruleset; default is a built-in ruleset tuned for generated specs
- Parse controller ASTs and extract routes, params, and return types.
- Resolve DTO imports against the project root.
- Build
components.schemasfrom TypeScript declarations. - Attach JSDoc/TSDoc block comments to operation and schema descriptions.
- Emit a standalone OpenAPI JSON document (
3.0.0by default, or3.1.0when configured).
Set "openApiVersion": "3.1" to emit openapi: 3.1.0 with jsonSchemaDialect and JSON Schema–style nullability:
nullable: trueon a string →type: ["string", "null"]- nullable
$ref→anyOf: [{ $ref: ... }, { type: "null" }] Promise<null>responses →type: "null"
Internal generation still uses OpenAPI 3.0 nullable until the final serialization pass, so 3.0 remains the default for existing tooling.
Leading /** ... */ block comments are matched to the next declaration by source position:
- Controller methods →
summary(first sentence) anddescription(full text when longer) - Classes, interfaces, and type aliases → schema
description - Properties and fields → property
description
Controller method tags are also translated:
@param name ...→parameters[].description(matched by TS variable name or HTTP name) orrequestBody.descriptionfor@Bodyparams@returns/@return→responses['200'].description@throws NotFoundException ...→responses['404'].description(NestJSHttpExceptionnames are mapped to status codes)
Config parameterDescriptions are used as a fallback when @param is absent.
Beyond @throws, the generator infers responses from controller code:
throw new NotFoundException(...)and similar NestJS HTTP exceptionsgetOrUnauthorized()/getOrBadRequest()helper calls@UseGuards(BearerGuard)→401, throttling guards →429@HttpCode(HttpStatus.CREATED)→ success response uses201instead of200
@throws JSDoc descriptions override inferred defaults when both are present.
Optional config keys guardStatusCodes and exceptionDescriptions can customize mappings.
@UseGuards(...) decorators are mapped to OpenAPI security:
BearerGuard→security: [{ bearerAuth: [] }]AnonymousOrBearerGuard→ optional bearer auth:security: [{ bearerAuth: [] }, {}]- Throttling / rate-limit guards affect
401/429responses only (notsecurity)
Defaults are emitted under components.securitySchemes.bearerAuth (HTTP bearer JWT).
Customize via config:
securitySchemes— declare or override scheme definitionsguardSecuritySchemes— map guard class names to scheme names (or{ scheme, optional })skipSecurityDecorators— decorator names (e.g.Public) that suppress security on a route
export enum OrderType { ... }→components.schemas.OrderTypewithenumvaluesexport const gender = ['F', 'M'] as const+type Gender = (typeof gender)[number]→ string enum schema- Class fields with
type = 'film-list' as constcontinue to produce discriminatedoneOfvariants
DTO class properties are enriched from class-validator (and class-transformer) decorators:
@IsOptional()/@IsNotEmpty()—requiredon the parent schema@IsString(),@IsNumber(),@IsBoolean(),@IsArray(),@IsObject(),@IsInt()@IsEmail(),@IsDateString(),@IsUUID(),@IsUrl()—format@Min(),@Max(),@MinLength(),@MaxLength(),@Length(),@ArrayMinSize(),@ArrayMaxSize()@IsEnum(MyEnum)/@IsIn(constArray)—enum(const arrays resolved via imports)@ValidateNested()+@Type(() => ChildDto)— nested$ref/ array items@Exclude()— property omitted from the schema
Common TypeScript utility types are resolved structurally:
Partial<T>— same properties, norequiredarrayRequired<T>— all properties marked requiredPick<T, 'a' | 'b'>/Omit<T, 'a' | 'b'>— subset of object propertiesRecord<K, V>—additionalPropertiesschema (unknown→ open object)Array<T>/ReadonlyArray<T>—type: arraywithitemsReadonly<T>— same asTin OpenAPI output
Single type-parameter classes such as CollectionDto<T> and TaxonomyDto<T> are instantiated at use sites:
CollectionDto<CinemaGroupDto>→components.schemas.CollectionDto_CinemaGroupDtowithdata: CinemaGroupDto[]class Foo extends TaxonomyDto<string[]>→$reftoTaxonomyDto_Array_string- Union type args:
CollectionDto<A | B>→items.oneOffor array elements
Bare generic templates (e.g. CollectionDto without type args) are not registered as empty components.
- Structural AST parsing only — no TypeScript type checker.
- Request bodies are inferred from
@Body()parameter types only (application/json). - Indexed access (
Foo['bar']), conditional types, and multi-parameter generics (Map<K,V>beyond utility types) are not fully resolved. - Class and interface
extendsemitallOf: [{ $ref: Parent }, { properties: ... }](child-only properties in the second fragment). - External package types fall back to open objects.
- Error responses are not inferred from thrown exceptions.