Files
What you'll learn
- Explain what an OpenAPI document is, who reads it (people through the Swagger UI, tools through the JSON) and where the Swagger module gets it from
- Describe an API with DocumentBuilder and mount the document with SwaggerModule.setup, built lazily through a factory
- Make a DTO's properties appear in its schema with @ApiProperty() and @ApiPropertyOptional(), and read what the document says about them
- Tune the document with SwaggerDocumentOptions, naming operations with an operationIdFactory
Introduction
The shelter's cats API has grown through five courses, and nobody outside your editor knows what it looks like. The volunteer writing the mobile app asks what POST /cats expects; the person building the admin dashboard wants to know which routes exist and what a 404 body looks like; a partner wants to generate a client. You could write a page by hand and watch it drift from the code within a week. Or you could let the code describe itself.
That description has a standard: OpenAPI, a JSON (or YAML) document listing every path, every operation on it, its parameters, bodies, responses and the schemas they share. Tools read it: Swagger UI renders it as a page to click through and send requests from, client generators turn it into typed SDKs, gateways validate against it. Nest's @nestjs/swagger generates the document from what it already knows about your app, so the document is the code.
Building the document
Two objects do the work in main.ts. A DocumentBuilder holds what the code cannot know: the API's title, a description, its version, the tags routes will be grouped under. SwaggerModule.createDocument() walks the application's routes and merges what it finds with that base:
const config = new DocumentBuilder()
.setTitle('Bookshop')
.setDescription('Orders and stock')
.setVersion('2.3')
.addTag('orders')
.build();
const documentFactory = () => SwaggerModule.createDocument(app, config);
SwaggerModule.setup('docs', app, documentFactory);
setup() takes a path, the app, and either the document or a factory that builds it. Prefer the factory: the document is built the first time somebody asks for it, which keeps startup fast and still sees versioning enabled after setup. Under that path setup() mounts two things: the Swagger UI page at /docs, whose script embeds the document, and the raw document at /docs-json and /docs-yaml, where tools fetch it. Here, the request bar shows you the JSON, and the "Open in a new tab" link above it opens the UI page.
What the scanner sees, and what it cannot
The scanner reads route decorators, so paths and methods come free: @Get(':id') in a @Controller('books') becomes GET /books/{id} with a required path parameter, and a @Body() parameter becomes a request body referencing the DTO's schema by name. But that schema is empty. TypeScript keeps a property's type as metadata only where a decorator asks for it, and a plain title: string; has none, so at runtime the class has no visible properties. @ApiProperty() is that request:
export class CreateBookDto {
@ApiProperty()
title: string;
@ApiProperty({ description: 'Pages, including the index', minimum: 1 })
pages: number;
@ApiPropertyOptional()
subtitle?: string;
}
Now the schema lists title as a string and pages as a number with a minimum, both in the required list, and subtitle as a string that is not required. @ApiPropertyOptional() is shorthand for @ApiProperty({ required: false }); the ? on subtitle is invisible to the scanner, only the decorator says it is optional. (The Nest CLI has a compiler plugin that writes these decorators for you; lesson 5 says what it does and why this browser, like ts-jest, compiles without it.)
The document describes; it does not enforce. A minimum: 1 in the schema tells a reader what is valid, and a ValidationPipe with @Min(1) is what refuses 0. Both belong on the DTO, and when they disagree the document makes the bug visible.
Naming operations
Every operation gets an operationId, the name a client generator turns into a method; the default is ControllerName_methodName. The third argument of createDocument() is a SwaggerDocumentOptions object, and its operationIdFactory receives the controller key and the method key and returns the id you want:
SwaggerModule.createDocument(app, config, {
operationIdFactory: (controllerKey: string, methodKey: string) => methodKey,
});
The same object takes include, extraModels and ignoreGlobalPrefix, which later lessons use.
Your task
The cats API is in place: four routes over a TypeORM entity, a CreateCatDto validated by the global ValidationPipe. Nothing describes it yet.
- In
main.ts, describe the API with aDocumentBuilder: titleShelter cats, descriptionThe shelter's cats API, version1.0, and acatstag. Build the document in a factory and mount it atapi. Run, and fetch/api-jsonin the request bar: the four routes are there, andCreateCatDtois an empty object. - Make the DTO's three properties visible to the document.
nameandageare required;breedis optional, and the document must say so. - Name each operation after its handler, so the document says
findAllandcreate, notCatsController_findAll.
Open the app in a new tab and go to /api to see the page the volunteer will read.
When it fails
- The document lists the routes but
CreateCatDtois{ "type": "object", "properties": {} }: the decorators are missing. Class-validator's@IsString()tells the pipe about the property, not the document; only@ApiProperty()does. breedappears in therequiredlist: it has@ApiProperty()where it needs@ApiPropertyOptional(). The?in the TypeScript type is not read at runtime.GET /apianswersCannot GET /api:setup()was never called, or was called with a different path. The document lives at<path>-json, sosetup('docs', ...)serves/docs-json, not/api-json.
Remember
DocumentBuilderholds what the code cannot know;createDocument()adds what it can;setup()serves both the UI and the JSON.- Pass a factory to
setup(), so the document is built on first request. @ApiProperty()on every DTO property,@ApiPropertyOptional()on the optional ones; without them a schema is empty.operationIdFactorynames operations for the clients that will be generated from the document.
Stuck? Show a hint
main.ts: import { DocumentBuilder, SwaggerModule } from '@nestjs/swagger'; const config = new DocumentBuilder().setTitle(...).setDescription(...).setVersion('1.0').addTag('cats').build(); SwaggerModule.setup('api', app, () => SwaggerModule.createDocument(app, config, { operationIdFactory: (controllerKey, methodKey) => methodKey })). create-cat.dto.ts: @ApiProperty() above name and age, @ApiPropertyOptional() above breed, both from '@nestjs/swagger'.
Press Run tests to start the app. Its log appears here.Graded endpoints
SwaggerModule.setup at 'api' serves the generated document at /api-json; its info section is what the DocumentBuilder was told
The scanner found all four routes; with the operationIdFactory each operation is named after its method, and the POST body references the DTO's schema
@ApiProperty() makes name and age visible with their types and marks them required; @ApiPropertyOptional() adds breed without requiring it
The same setup serves the UI's HTML page at /api; it loads the document from its init script (open it in a new tab to see it rendered)
The UI does not fetch /api-json; the document is embedded in the script the page loads
Documenting changes nothing about the routes: a cat is created as before
A negative age is refused by the ValidationPipe, not by anything OpenAPI does; the document describes, the pipe enforces
The entity as stored, read by id; the document's GET /cats/{id} is this route
DELETE answers 204 with no body, which the document will describe in a later lesson
The cat is no longer there