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:
| Decorator | Status code | Typical use |
|---|---|---|
@ApiOkResponse() | 200 | Successful read or update |
@ApiCreatedResponse() | 201 | Resource created |
@ApiNoContentResponse() | 204 | Successful delete with no body |
@ApiBadRequestResponse() | 400 | Validation / malformed input |
@ApiUnauthorizedResponse() | 401 | Missing or invalid credentials |
@ApiForbiddenResponse() | 403 | Authenticated but not permitted |
@ApiNotFoundResponse() | 404 | Resource does not exist |
Declare error responses explicitly. The generated document never invents
400or404entries 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
@ApiTagsat the controller level so all of a resource’s routes group together automatically. - Give every handler an
@ApiOperationsummary — 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
typeon success responses so Swagger renders a concrete example schema rather than an empty object. - Use
enumandexamplevalues on@ApiQuery/@ApiParamto make the “Try it out” experience accurate and self-explanatory. - Set a stable
operationIdwhen you generate typed client SDKs, so regenerated clients keep consistent method names.