Generating the Spec
Build an OpenAPI document from the code you already wrote, then serve it with @nestjs/swagger.
SwaggerModule.setup(path: string, app: INestApplication, documentOrFactory: OpenAPIObject | (() => OpenAPIObject), options?: SwaggerCustomOptions): voidAn OpenAPI document is a machine-readable description of an HTTP API: every path, every parameter, every response shape, in one JSON or YAML file. The @nestjs/swagger package builds that file from the application you already wrote, so the description cannot quietly fall behind the code the way a hand-written one does.
How the document is built
Install it with npm install --save @nestjs/swagger. On Fastify you also need @fastify/static, because the UI is served as static files. DocumentBuilder holds the parts that are not in your code at all: title, version, servers, security schemes. Its build() returns the whole document except paths. SwaggerModule.createDocument(app, config) fills that in by walking the module graph. A module is the class that groups controllers and providers; a provider is any class Nest constructs and hands to whoever asks for it. For each controller method the scanner reads the metadata that @Get, @Param, @Body and the @Api* decorators left behind, and writes one operation per route.
When it runs
None of this is per-request work. Decorators store metadata once, when the class is first evaluated. The scan runs once, inside createDocument. Passing a factory function to setup instead of a finished document delays that scan until the first request for the docs page.
What trips people up
Nest 12 serves Swagger UI at the path you pass and the raw spec at <path>-json and <path>-yaml. If request bodies come out empty, nothing is broken: TypeScript types are erased at compile time, so a DTO class contributes no schema until @ApiProperty or the CLI plugin annotates it.
Example
// src/main.ts
import { NestFactory } from '@nestjs/core';
import { DocumentBuilder, SwaggerModule } from '@nestjs/swagger';
import { AppModule } from './app.module';
async function bootstrap() {
const app = await NestFactory.create(AppModule);
const config = new DocumentBuilder()
.setTitle('Orders API')
.setDescription('Internal order service')
.setVersion('1.0') // info.version: YOUR api version, not the OpenAPI version
.setOpenAPIVersion('3.1.0') // optional; DocumentBuilder defaults to 3.0.0
.addTag('orders')
.addBearerAuth() // registers a security scheme named 'bearer'
.addGlobalResponse({ status: 500, description: 'Internal server error' })
.build(); // returns the document minus `paths`
// Shorthand: hand setup() a factory and the route scan is deferred
// until the first request for /docs.
const documentFactory = () => SwaggerModule.createDocument(app, config);
SwaggerModule.setup('docs', app, documentFactory, {
// Without these two the spec lives at /docs-json and /docs-yaml.
jsonDocumentUrl: 'docs/openapi.json',
yamlDocumentUrl: 'docs/openapi.yaml',
customSiteTitle: 'Orders API docs'
});
await app.listen(process.env.PORT ?? 3000);
}
bootstrap();When to use it
- Publish a browsable docs page for an internal service so other teams stop reading your controllers.
- Export openapi.json in CI and commit it, so a reviewer can see the API diff in a pull request.
- Hide an internal admin module from a public spec with the `include` option.
- Disable the Swagger UI in production while keeping the JSON endpoint for tooling.
More examples
Longhand: build the document eagerly
SwaggerModule.setup accepts either a finished OpenAPIObject or a function returning one, and both forms produce the same document. Reach for the eager form when something else in main.ts needs the document object itself, for example to write it to disk or hand it to a diffing step. Note that `ui` and `raw` default to true, so an unguarded setup publishes your whole API surface in production.
// src/main.ts
import { NestFactory } from '@nestjs/core';
import { DocumentBuilder, SwaggerModule } from '@nestjs/swagger';
import { AppModule } from './app.module';
async function bootstrap() {
const app = await NestFactory.create(AppModule);
const config = new DocumentBuilder()
.setTitle('Orders API')
.setVersion('1.0')
.build();
// Longhand of the factory form: the scan happens here, at startup.
const document = SwaggerModule.createDocument(app, config);
SwaggerModule.setup('docs', app, document, {
// `ui` turns the HTML explorer on or off; `raw` controls the JSON/YAML
// endpoints. Both default to true.
ui: process.env.NODE_ENV !== 'production',
raw: ['json']
});
await app.listen(3000);
}
bootstrap();Let the CLI plugin write the decorators
The plugin is a TypeScript transformer that runs during compilation and inserts @ApiProperty metadata for you, using the property types the compiler already knows. Defaults: dtoFileNameSuffix is ['.dto.ts', '.entity.ts'], controllerFileNameSuffix is ['.controller.ts'], classValidatorShim is true and introspectComments is false. Files that do not match those suffixes are ignored, which is the usual reason one DTO documents itself and another stays blank. If you build with SWC or run under ts-jest the transformer does not run, so generate metadata.ts and load it with `await SwaggerModule.loadPluginMetadata(metadata)` before createDocument.
// nest-cli.json
{
"$schema": "https://json.schemastore.org/nest-cli",
"collection": "@nestjs/schematics",
"sourceRoot": "src",
"compilerOptions": {
"plugins": [
{
"name": "@nestjs/swagger",
"options": {
"dtoFileNameSuffix": [".dto.ts", ".entity.ts"],
"controllerFileNameSuffix": [".controller.ts"],
"classValidatorShim": true,
"introspectComments": true
}
}
]
}
}Controlling what gets scanned
SwaggerDocumentOptions is the third argument to createDocument and decides what the scanner looks at and how it names things. `include` is the switch to reach for when an internal admin module must not appear in a public spec. operationIdFactory matters most because client generators turn operation ids into function names, so pick its shape before anyone generates a client from the spec.
// src/main.ts (excerpt)
import { SwaggerDocumentOptions, SwaggerModule } from '@nestjs/swagger';
import { OrdersModule } from './orders/orders.module';
const options: SwaggerDocumentOptions = {
// Only scan these modules; everything else is left out of the document.
include: [OrdersModule],
// Follow the imported modules of the included ones too.
deepScanRoutes: true,
// Register schemas that no handler signature mentions directly.
extraModels: [],
// Default operationId is `OrdersController_findAll`; this drops the class
// name and yields `findAll`, versioned as `findAll_1` when versioning is on.
operationIdFactory: (controllerKey, methodKey, version) =>
version ? `${methodKey}_${version}` : methodKey,
// Default is true: each controller gets a tag named after its class.
autoTagControllers: false
};
const document = SwaggerModule.createDocument(app, config, options);
Discussion