Skip to content

API Documentation with Swagger

Aman Kumar Singh7 min read
Part 28 of 40From the NestJS Production Guide series
API Documentation with Swagger — article by Aman Kumar Singh

Last time we wired up Health Checks and Readiness Probes so the platform could tell your orchestrator whether the API was actually fit to receive traffic. That's infrastructure talking to infrastructure. This time we're solving a different problem: getting your API to explain itself to the humans and other services that need to call it.

This is part of the NestJS Production Guide series. Everything so far has been about the app surviving contact with production. Documentation is the other half of the job: making sure the contract you've built is legible to the frontend team, a partner integrating against your API, or you in six months trying to remember what a field actually means.

Most teams treat API docs as an afterthought: something written once near launch and never touched again. That's exactly why it rots. Discipline won't fix that. Generating the docs from the same code that serves the requests will, because then they can't drift out from under you.

Why hand-written docs fail

A Postman collection or a Notion page describing your endpoints is accurate on the day you write it and wrong a month later. Someone adds a field to a DTO, renames a query param, or changes a status code, and the doc doesn't know. Nobody remembers to update it because updating docs isn't part of the change that broke them.

The only way documentation survives is if it's derived from the same source of truth as the code path it describes. @nestjs/swagger reads your controllers, DTOs, and decorators and builds an OpenAPI 3 document from them at startup. When the DTO changes, the doc changes automatically the next time the app boots. That single property, generated instead of maintained, is the entire reason to reach for it over a wiki page.

It also gives you something a wiki page can't: a machine-readable OpenAPI spec. Once you have that JSON document, you can feed it into client SDK generators, contract tests, or a mock server for the frontend team to build against before your endpoint is even done.

Wiring up @nestjs/swagger

Installation is one package, and the setup lives entirely in main.ts.

npm install @nestjs/swagger
// 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('Billing API')
    .setDescription('Internal and partner-facing endpoints for subscriptions and invoices')
    .setVersion('1.0')
    .addBearerAuth()
    .build();

  const document = SwaggerModule.createDocument(app, config);
  SwaggerModule.setup('docs', app, document);

  await app.listen(3000);
}
bootstrap();

DocumentBuilder describes the API as a whole: its title, version, and the auth scheme it expects. createDocument walks every controller NestJS knows about and assembles the OpenAPI document from decorators and type metadata. SwaggerModule.setup('docs', ...) mounts the interactive UI at /docs and, by default, the raw JSON spec at /docs-json, which is the file you'd hand to a codegen tool or an external partner.

Note that without any further decorators, this already produces a usable document: NestJS infers request and response shapes from your DTOs and TypeScript types. The decorators below exist to add the parts TypeScript can't express, like descriptions, examples, and which responses map to which status codes.

Documenting DTOs and endpoints properly

The baseline document from reflection alone is thin: it knows a field exists and its primitive type, not what it means. @ApiProperty closes that gap on your DTOs.

// src/modules/invoices/dto/create-invoice.dto.ts
import { ApiProperty } from '@nestjs/swagger';
import { IsInt, IsPositive, IsString } from 'class-validator';

export class CreateInvoiceDto {
  @ApiProperty({ description: 'Stripe customer ID this invoice belongs to' })
  @IsString()
  customerId: string;

  @ApiProperty({ description: 'Amount in cents, always positive', example: 4999 })
  @IsInt()
  @IsPositive()
  amountCents: number;

  @ApiProperty({ description: 'ISO currency code', example: 'usd' })
  @IsString()
  currency: string;
}

The class-validator decorators you're already using for request validation and the Swagger decorators live side by side on the same properties. That's deliberate: the validation rules and the documented shape describe the same contract, so keeping them on the same class means they can't quietly diverge from each other, even if they can still diverge from reality if nobody updates either.

Controllers get their own layer of decorators, describing what an operation does and what it returns for each outcome.

// src/modules/invoices/invoices.controller.ts
import { Body, Controller, Post, UseGuards } from '@nestjs/common';
import {
  ApiBearerAuth,
  ApiOperation,
  ApiResponse,
  ApiTags,
} from '@nestjs/swagger';
import { JwtAuthGuard } from '../auth/jwt-auth.guard';
import { CreateInvoiceDto } from './dto/create-invoice.dto';
import { InvoicesService } from './invoices.service';

@ApiTags('invoices')
@ApiBearerAuth()
@UseGuards(JwtAuthGuard)
@Controller('invoices')
export class InvoicesController {
  constructor(private readonly invoices: InvoicesService) {}

  @Post()
  @ApiOperation({ summary: 'Create an invoice for a customer' })
  @ApiResponse({ status: 201, description: 'Invoice created' })
  @ApiResponse({ status: 400, description: 'Invalid payload' })
  @ApiResponse({ status: 402, description: 'Payment method declined' })
  create(@Body() dto: CreateInvoiceDto) {
    return this.invoices.create(dto);
  }
}

@ApiTags groups this controller under a heading in the UI so the sidebar reads as a list of resources instead of a flat list of routes. @ApiBearerAuth tells Swagger UI to show an "Authorize" button and attach the token to every request made from the docs, matching the addBearerAuth() call in main.ts. @ApiOperation and @ApiResponse are the parts reflection genuinely can't infer: what a 402 means for this specific endpoint isn't something the type system knows, so you have to say it.

You don't need every decorator on every route from day one. Start with @ApiTags and a couple of @ApiResponse entries for the status codes that actually matter, the ones a caller needs to branch on. Add the rest as questions come in from whoever's consuming the API.

Keeping docs out of production, or not

Whether /docs should exist in production is a real decision, not a default. For an internal API only your own frontend calls, leaving Swagger UI open in production is low risk and genuinely convenient: any engineer can check the contract without digging through source. For a partner-facing or public API, an unauthenticated /docs route is a way to hand an attacker your entire surface area, request shapes and all, without them writing a line of reconnaissance code.

The middle ground I use most often is gating the docs route behind the same auth the rest of the API uses, or behind a separate environment check.

// src/main.ts
if (process.env.NODE_ENV !== 'production' || process.env.ENABLE_DOCS === 'true') {
  SwaggerModule.setup('docs', app, document);
}

That's a blunt tool, an environment variable, but it matches the actual risk: docs are a developer convenience in staging and a potential liability in production for an externally reachable API. If partners genuinely need the docs, put them behind the same bearer auth or API key you'd require for any other endpoint, rather than leaving the route open.

Production pitfalls worth knowing up front

The most common failure mode is a specific kind of staleness, not a misconfiguration: someone adds a new required field to a request DTO but forgets the @ApiProperty() decorator. The endpoint still works and the runtime validation still enforces the field, but the generated document doesn't show it, so any client generated from that spec breaks in a way that looks like a Swagger bug and is actually a missing decorator.

A related trap is documenting a response shape that doesn't match what the service actually returns, usually because someone changed a service method's return type without touching the controller's @ApiResponse type reference. @ApiResponse({ type: SomeDto }) doesn't validate that the handler actually returns that shape; it just tells the document what to claim. The two can drift silently unless your e2e tests actually assert on response shape.

If you're generating a client SDK from the OpenAPI JSON, treat spec changes as you would any other contract change: review the diff in CI before merging, not after a downstream consumer's build breaks. A cheap check is running SwaggerModule.createDocument in a test and diffing the serialized JSON against a committed snapshot, which catches accidental breaking changes to the contract the same way a snapshot test catches accidental UI changes.

Key takeaways

  • Generate documentation from the code that actually serves requests; hand-maintained docs will always drift because updating them isn't part of the change that breaks them.
  • `@nestjs/swagger`'s `DocumentBuilder` and `SwaggerModule` produce both an interactive UI and a raw OpenAPI JSON document you can feed into codegen or contract tests.
  • Keep `@ApiProperty` and class-validator decorators on the same DTO properties so validation rules and documented shape describe one contract, not two.
  • Decide deliberately whether `/docs` belongs in production; gate it behind auth or an environment flag for anything externally reachable.
  • Watch for silent drift: a missing `@ApiProperty` or a stale `@ApiResponse` type won't break the build, only mislead whoever reads the spec.
  • Snapshot-test the generated OpenAPI document if you're shipping a client SDK from it, so contract changes show up in code review.

Frequently asked questions

Do I need @nestjs/swagger if I already have a Postman collection?

A Postman collection maintained by hand has the same staleness problem as any other manual doc. `@nestjs/swagger` generates from your actual controllers and DTOs, so it stays accurate as the code changes. You can still import the generated OpenAPI JSON into Postman if your team prefers that interface.

Does adding Swagger decorators slow down my endpoints?

No. The decorators only add metadata that NestJS reads once, at startup, to build the document. They don't run on the request path at all, so there's no runtime cost to documenting a route.

Should Swagger UI be exposed in production?

It depends on who can reach your API. For an internal service only your own frontend calls, leaving it open is usually fine and convenient. For a public or partner-facing API, gate it behind the same auth as the rest of your endpoints, or disable it outright and share the OpenAPI JSON directly with partners who need it.

How do I document authentication in Swagger?

Call `.addBearerAuth()` on your `DocumentBuilder` and add `@ApiBearerAuth()` to the controllers or routes that require it. Swagger UI then shows an "Authorize" button that attaches the token to every request made from the docs, matching how a real client would call the endpoint.

Can I generate a TypeScript client from the OpenAPI document?

Yes, tools like `openapi-typescript-codegen` or `orval` can consume the JSON that `SwaggerModule` produces and generate typed clients for your frontend. Treat the generated spec as a contract: review its diff in CI so a breaking change to a DTO doesn't silently break every consumer downstream.

What's the difference between class-validator decorators and Swagger decorators?

class-validator decorators like `@IsString()` enforce validation rules at runtime against incoming requests. `@ApiProperty()` only describes that same field for the generated document; it has no effect on validation. You typically want both on a DTO property, since one enforces the contract and the other documents it.

Related articles

Security Best Practices — Aman Kumar Singh
Rate Limiting and Throttling — Aman Kumar Singh
Sending Emails from NestJS — Aman Kumar Singh
Aman Kumar Singh

About the author

I'm Aman Kumar Singh, a software engineer in Noida, India building scalable full-stack products with React, Next.js, Node.js, NestJS, PostgreSQL, Redis, and AWS. I write about backend engineering, distributed systems, and system design.