Documenting Responses
Declare the status codes, body types and error shapes your handlers really return, with usable examples.
@ApiResponse({ status, description?, type?, isArray?, example? | examples? })The request half of a spec is mostly inferred. The response half is not: Nest knows a handler returns a Promise and nothing more, so every status code and body shape has to be declared. @ApiResponse is that declaration, and the per-status shortcuts are the same function with the status pre-filled — @ApiOkResponse(o) is @ApiResponse({ ...o, status: 200 }), @ApiNotFoundResponse(o) is status 404, and so on for every member of HttpStatus.
Where the real status comes from
These decorators document; they never enforce. The actual status is decided at the end of the request lifecycle. Middleware runs first, then guards, then interceptors, then pipes, then your handler; if the handler throws, an exception filter — a class that converts a thrown exception into an HTTP response — produces the body instead, and interceptors on the way out may still reshape it. Nest answers 201 for @Post and 200 for the other methods by default, and @HttpCode(204) changes that. If your decorator and that chain disagree, the spec is simply wrong and nothing will tell you.
Errors are part of the contract
Document the failures a client must handle, and document the body it will really receive. A ValidationPipe rejection, for example, is a 400 whose message is an array of strings rather than a single one. Describe that shape with a DTO so a generated client can narrow on it.
Example
// src/orders/orders.controller.ts
import {
Body,
Controller,
Get,
HttpCode,
Param,
ParseUUIDPipe,
Post
} from '@nestjs/common';
import {
ApiBadRequestResponse,
ApiCreatedResponse,
ApiNoContentResponse,
ApiNotFoundResponse,
ApiOkResponse,
ApiOperation,
ApiTags
} from '@nestjs/swagger';
import { CreateOrderDto } from './dto/create-order.dto';
import { ValidationErrorDto } from './dto/validation-error.dto';
import { OrderEntity } from './entities/order.entity';
import { OrdersService } from './orders.service';
ApiTags('orders')
Controller('orders')
export class OrdersController {
constructor(private readonly orders: OrdersService) {}
Post()
ApiOperation({ summary: 'Place an order', operationId: 'createOrder' })
// Shorthand for @ApiResponse({ status: 201, ... }) — @Post answers 201.
ApiCreatedResponse({ description: 'Order accepted', type: OrderEntity })
// The body ValidationPipe really sends on a rejected payload.
ApiBadRequestResponse({ description: 'Invalid body', type: ValidationErrorDto })
create(Body() dto: CreateOrderDto): Promise<OrderEntity> {
return this.orders.create(dto);
}
Get(':id')
ApiOkResponse({ type: OrderEntity })
ApiNotFoundResponse({ description: 'No order with that id' })
findOne(Param('id', ParseUUIDPipe) id: string): Promise<OrderEntity> {
return this.orders.findOne(id);
}
Get()
// isArray wraps the referenced schema in an array; the longhand is
// @ApiResponse({ status: 200, schema: { type: 'array', items: { $ref: ... } } }).
ApiOkResponse({ type: OrderEntity, isArray: true })
findAll(): Promise<OrderEntity[]> {
return this.orders.findAll();
}
Post(':id/cancel')
HttpCode(204) // without this, @Post would answer 201
ApiNoContentResponse({ description: 'Cancelled; no body returned' })
cancel(Param('id', ParseUUIDPipe) id: string): Promise<void> {
return this.orders.cancel(id);
}
}When to use it
- Tell a mobile client which status codes an endpoint can return before they write the error handling.
- Document the 400 body ValidationPipe produces so clients can show field-level messages.
- Describe a paginated envelope that TypeScript generics cannot express as a schema.
- Mark an endpoint deprecated in the spec while its replacement is rolled out.
More examples
An error DTO that matches the real body
Two different error shapes come out of a default Nest app, and clients break on the difference: code written for `message: string` throws when it meets the validation pipe's array. Declaring both DTOs and pointing the right one at each status is what lets a typed client narrow the error correctly. If you install a global exception filter that normalises errors into one envelope, document that envelope instead and delete these.
// src/orders/dto/validation-error.dto.ts
import { ApiProperty } from '@nestjs/swagger';
// Mirrors what the built-in ValidationPipe throws: a BadRequestException
// whose `message` is the list of failed constraints, one string each.
export class ValidationErrorDto {
@ApiProperty({ example: 400 })
statusCode: number;
@ApiProperty({
type: [String],
example: ['customer must be shorter than or equal to 40 characters']
})
message: string[];
@ApiProperty({ example: 'Bad Request' })
error: string;
}
// src/orders/dto/http-error.dto.ts
import { ApiProperty } from '@nestjs/swagger';
// Every other HttpException: `message` is a single string, not an array.
export class HttpErrorDto {
@ApiProperty({ example: 404 })
statusCode: number;
@ApiProperty({ example: 'No order with that id' })
message: string;
}A generic envelope with getSchemaPath
TypeScript generics vanish at runtime, so Page<OrderEntity> cannot be inferred into a schema. The `schema` form of @ApiResponse is the way out: compose the envelope by hand and reference the item type with getSchemaPath. Factor it into a small decorator factory once more than two endpoints paginate, otherwise this block gets copied and the copies drift.
// src/common/dto/page-meta.dto.ts
import { ApiProperty } from '@nestjs/swagger';
export class PageMetaDto {
@ApiProperty()
total: number;
@ApiProperty()
page: number;
}
// src/orders/orders-page.controller.ts
import { Controller, Get, Query } from '@nestjs/common';
import { ApiExtraModels, ApiOkResponse, getSchemaPath } from '@nestjs/swagger';
import { PageMetaDto } from '../common/dto/page-meta.dto';
import { OrderEntity } from './entities/order.entity';
// Register both schemas: the raw definition below only refers to them by $ref,
// so the scanner has no other way to learn they exist.
@ApiExtraModels(PageMetaDto, OrderEntity)
@Controller('orders')
export class OrdersPageController {
@Get('page')
@ApiOkResponse({
description: 'One page of orders',
// The `schema` form of @ApiResponse: a raw OpenAPI schema, not a class.
schema: {
type: 'object',
required: ['data', 'meta'],
properties: {
data: {
type: 'array',
items: { $ref: getSchemaPath(OrderEntity) }
},
meta: { $ref: getSchemaPath(PageMetaDto) }
}
}
})
page(@Query('page') page = '1') {
return { data: [], meta: { total: 0, page: Number(page) } };
}
}Named examples a client author can paste
An example is the part of a spec a client author actually copies, and an empty-result example is worth as much as a populated one, because that is the case callers forget to handle. @ApiOperation sets the operation's summary, description, deprecated flag and operationId; the operationId is what client generators turn into a method name, so setting it by hand pins that name against later refactors.
// src/orders/orders-search.controller.ts
import { Controller, Get, Query } from '@nestjs/common';
import { ApiOkResponse, ApiOperation, ApiQuery } from '@nestjs/swagger';
import { OrderEntity } from './entities/order.entity';
@Controller('orders')
export class OrdersSearchController {
@Get('search')
@ApiOperation({
summary: 'Search orders',
description: 'Results are capped at 100 rows.',
deprecated: true // struck through in the UI and marked in the spec
})
@ApiQuery({ name: 'status', enum: ['open', 'shipped'], required: false })
@ApiOkResponse({
type: OrderEntity,
isArray: true,
// Each entry needs BOTH summary and value; the UI shows summary in the
// dropdown. Use `example` (singular) for a bare value with no label.
examples: {
empty: { summary: 'No matches', value: [] },
one: {
summary: 'A single open order',
value: [
{
id: '6f1a2c3e-0b4d-4f6a-9c1e-8d2b5a7f0c11',
customer: 'ACME Ltd',
createdAt: '2026-01-14T09:30:00.000Z'
}
]
}
}
})
search(@Query('status') status?: string) {
return [];
}
}
Discussion