IntroductionOpenAPI · NestJS

Generate an OpenAPI document for the shelter's cats API from the code it already has: describe it with a DocumentBuilder, mount the Swagger UI at /api, make the DTO's properties visible to the document, and name each operation after its handler.

What you will learn

Read the theory for Introduction

All OpenAPI lessons

All NestJS courses

loading types…

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.

  1. In main.ts, describe the API with a DocumentBuilder: title Shelter cats, description The shelter's cats API, version 1.0, and a cats tag. Build the document in a factory and mount it at api. Run, and fetch /api-json in the request bar: the four routes are there, and CreateCatDto is an empty object.
  2. Make the DTO's three properties visible to the document. name and age are required; breed is optional, and the document must say so.
  3. Name each operation after its handler, so the document says findAll and create, not CatsController_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 CreateCatDto is { "type": "object", "properties": {} }: the decorators are missing. Class-validator's @IsString() tells the pipe about the property, not the document; only @ApiProperty() does.
  • breed appears in the required list: it has @ApiProperty() where it needs @ApiPropertyOptional(). The ? in the TypeScript type is not read at runtime.
  • GET /api answers Cannot GET /api: setup() was never called, or was called with a different path. The document lives at <path>-json, so setup('docs', ...) serves /docs-json, not /api-json.

Remember

  • DocumentBuilder holds 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.
  • operationIdFactory names 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'.