Skip to content
NestJS ns openapi 4 min read

Documenting Endpoints

Once the Swagger module is wired up, NestJS infers a great deal from your route decorators and DTOs automatically — but the auto-generated document is generic. The decorators in @nestjs/swagger let you layer human-readable intent on top: a summary for each operation, the exact set of responses a client should expect, and descriptions for every path and query parameter. Rich annotations turn a bare schema into a self-explanatory contract that frontend teams and external integrators can consume without reading your source.

Grouping operations with @ApiTags

@ApiTags groups related endpoints under a named heading in the Swagger UI sidebar. Apply it at the controller level so every route inside inherits the tag, keeping the generated document organized by resource.

// users.controller.ts
import { Controller, Get, Post, Param, Query, Body } from '@nestjs/common';
import { ApiTags } from '@nestjs/swagger';
import { UsersService } from './users.service';
import { CreateUserDto } from './dto/create-user.dto';

@ApiTags('users')
@Controller('users')
export class UsersController {
  constructor(private readonly users: UsersService) {}

  @Get()
  findAll(@Query('role') role?: string) {
    return this.users.findAll(role);
  }
}

Every handler in UsersController now appears under a collapsible users group. You can pass multiple tags (@ApiTags('users', 'admin')) to surface a route under several headings.

Describing operations with @ApiOperation

@ApiOperation documents what a single handler does. The summary shows next to the route in the UI, while description supports a longer multi-line explanation. Set operationId when you generate client SDKs and want stable, predictable method names.

import { ApiOperation } from '@nestjs/swagger';

@Get(':id')
@ApiOperation({
  summary: 'Fetch a single user',
  description: 'Returns the full user record, including profile and role.',
  operationId: 'getUserById',
})
findOne(@Param('id') id: string) {
  return this.users.findOne(id);
}

Documenting responses with @ApiResponse

By default Swagger only knows about the success status code implied by your route (200 for GET, 201 for POST). Real APIs return errors too, and clients need to know them. Declare each possible outcome with @ApiResponse, or use the status-specific shorthands like @ApiOkResponse and @ApiNotFoundResponse.

import {
  ApiResponse,
  ApiCreatedResponse,
  ApiBadRequestResponse,
  ApiNotFoundResponse,
} from '@nestjs/swagger';
import { UserEntity } from './entities/user.entity';

@Post()
@ApiCreatedResponse({ description: 'User created', type: UserEntity })
@ApiBadRequestResponse({ description: 'Validation failed' })
create(@Body() dto: CreateUserDto) {
  return this.users.create(dto);
}

@Get(':id')
@ApiResponse({ status: 200, description: 'The user was found', type: UserEntity })
@ApiNotFoundResponse({ description: 'No user with that id' })
findOne(@Param('id') id: string) {
  return this.users.findOne(id);
}

The type option tells Swagger which schema models the response body so the UI can render an example. The shorthand decorators map to fixed status codes:

DecoratorStatus codeTypical use
@ApiOkResponse()200Successful read or update
@ApiCreatedResponse()201Resource created
@ApiNoContentResponse()204Successful delete with no body
@ApiBadRequestResponse()400Validation / malformed input
@ApiUnauthorizedResponse()401Missing or invalid credentials
@ApiForbiddenResponse()403Authenticated but not permitted
@ApiNotFoundResponse()404Resource does not exist

Declare error responses explicitly. The generated document never invents 400 or 404 entries on its own, so an undocumented error path is invisible to anyone reading the spec or generating a client.

Annotating parameters with @ApiParam and @ApiQuery

NestJS detects route parameters and query strings, but it cannot infer their meaning, type, or whether they are optional. @ApiParam documents path parameters and @ApiQuery documents query parameters, including enums, defaults, and required flags.

import { ApiParam, ApiQuery } from '@nestjs/swagger';

enum UserRole {
  Admin = 'admin',
  Member = 'member',
}

@Get(':id')
@ApiParam({ name: 'id', description: 'UUID of the user', example: 'a1b2-c3d4' })
findOne(@Param('id') id: string) {
  return this.users.findOne(id);
}

@Get()
@ApiQuery({ name: 'role', enum: UserRole, required: false })
@ApiQuery({ name: 'page', type: Number, required: false, example: 1 })
findAll(@Query('role') role?: UserRole, @Query('page') page = 1) {
  return this.users.findAll(role, page);
}

The enum option renders a dropdown of allowed values in the UI, and required: false marks the parameter as optional so clients are not forced to supply it.

Verifying the generated document

After annotating a few routes, start the app and inspect the JSON spec to confirm your descriptions and responses landed correctly.

curl -s http://localhost:3000/api-json | head -n 20

Output:

{
  "openapi": "3.0.0",
  "paths": {
    "/users/{id}": {
      "get": {
        "operationId": "getUserById",
        "summary": "Fetch a single user",
        "tags": ["users"],
        "responses": {
          "200": { "description": "The user was found" },
          "404": { "description": "No user with that id" }
        }
      }
    }
  }
}

Best Practices

  • Apply @ApiTags at the controller level so all of a resource’s routes group together automatically.
  • Give every handler an @ApiOperation summary — it is the first thing a consumer reads in the UI.
  • Document every realistic outcome, especially errors, with the status-specific response decorators instead of relying on defaults.
  • Set a type on success responses so Swagger renders a concrete example schema rather than an empty object.
  • Use enum and example values on @ApiQuery/@ApiParam to make the “Try it out” experience accurate and self-explanatory.
  • Set a stable operationId when you generate typed client SDKs, so regenerated clients keep consistent method names.
Last updated June 14, 2026
Was this helpful?