docfy patch-spec
Patches an already-built OpenAPI document, entirely via static analysis (ts-morph), with no require() of any docs file.
Why it exists
This is a manual, CI-driven way to work around the one thing DocfyModule's runtime pipeline structurally cannot do: work under NestJS CLI's webpack: true build mode (see The CLI plugin for the automatic, recommended alternative, which computes this same analysis on every build instead of a separate step). patch-spec sidesteps the webpack wall by matching on path + HTTP method, computed the same way check/coverage/lint already do, instead of needing the live controller class reference.
Usage
npx nestjs-docfy patch-spec --spec <path-or-url> [options]# Patch a running app's served document
npx nestjs-docfy patch-spec --spec http://localhost:3000/api-json --out openapi.json
# Patch a file already written to disk
npx nestjs-docfy patch-spec --spec dist/openapi.json --out dist/openapi.jsonOptions
| Option | Default | Description |
|---|---|---|
--spec <path|url> | (required) | A local openapi.json, or a URL (e.g. a running app's /api-json) |
--out <path> | stdout | Where to write the patched document |
--root <path> | . | Project root directory |
--tsconfig <path> | auto-detected | Path to tsconfig.json |
--pattern <glob> | **/*.controller.ts | Glob pattern to find controllers |
--format <format> | ts | Docs file format to look for: ts or js |
--quiet | false | Suppress all output except errors |
What gets merged
Matched by path + HTTP method against the base document:
ApiTags→ unioned intotags(never drops tags the base document already had)ApiOperation({ summary, description, deprecated })→ overwrites those fieldsApiResponse({ status, description, schema, example, examples })→ merged per status code (other status codes untouched); when noschema/typeis given, falls back to the method's own resolved return type, the same DTO/class-validator/interface inferencegeneratealready does, including aoneOfof$refs when the return type is a union of ≥2 named DTOs/entities (e.g.Promise<UserDto | AdminDto>)ApiBody({ schema, examples })→ setsrequestBody, with the same return-type-style fallback to the@Body()parameter's resolved type, including the same union →oneOfhandling (exampleis intentionally not read here, since@nestjs/swagger's realApiBodyOptionstype only hasexamples, unlikeApiResponse, which supports both)ApiBearerAuth()→ appended tosecurityApiParam/ApiQuery/ApiHeader→ merged intoparametersby name + location: a genuinely new parameter is appended, but one that already exists (e.g.@nestjs/swaggerauto-generates a barerequired: trueentry from reflection alone for any@Query()/@Param()-decorated argument) gets its fields overlaid, not discarded.enumis read from an array literal (enum: ['a', 'b']) or a reference to a TSenum, including one imported from another file (enum: Role), and the schema'stypeis inferred asnumberwhen every resolved value is numeric,stringotherwise
What this does not do
This command is intentionally scoped, not a full reimplementation of @nestjs/swagger's decorator semantics: anyOf (no TS construct maps onto "any of" the way a union type naturally maps onto oneOf), links/callbacks (@nestjs/swagger itself has no decorator option for either), and any decorator argument that isn't a literal (a variable, a function call, a spread) are left alone rather than guessed at. It's better to leave a field as the base document already had it than patch in something wrong. A union return type or @Body() payload only becomes a oneOf when every branch resolves to a named DTO/entity. Routes a docs file documents that don't exist in --spec are reported as warnings, not silently dropped or errored on.