Files
What you'll learn
- Group operations with @ApiTags and describe the tag in the DocumentBuilder; add a summary with @ApiOperation and a shared header with @ApiHeader
- Document responses with @ApiResponse and its status shorthands, with a model class as the type, and declare one response for every operation with addGlobalResponse
- Describe a response the scanner cannot see, a generic page of results, with @ApiExtraModels, getSchemaPath and allOf, packaged as a reusable decorator with applyDecorators
- Document a file upload as a multipart/form-data body with @ApiConsumes and a binary-string property
Operations
Schemas describe data; operations describe what happens. The admin dashboard's developer reads GET /cats and wants to know what it is for, what comes back on success, what a 404 looks like and whether there is a header they must send. The scanner knows the path and the method and, for a @Body(), the request schema, and that is all it knows: it cannot read a handler's return type (a Promise<Cat> is metadata for Promise, no more), it cannot see which exceptions a service throws, and it cannot guess what a route is for. The rest of the operation is yours to declare.
Tags, summaries, headers
@ApiTags('orders') on a controller (or a method) files every operation under that tag; the Swagger UI groups by tag, and the tag's description, given to addTag('orders', 'Everything a customer bought') in the DocumentBuilder, heads the group. Without @ApiTags the scanner tags by controller name, so OrdersController becomes Orders. @ApiOperation({ summary: 'Place an order' }) is the one line shown next to the method; a longer description can go with it. @ApiHeader({ name: 'X-Request-Id', description: '...' }) documents a request header, and on the controller it lands on every operation, ahead of the parameters the scanner found.
Responses
@ApiResponse({ status: 201, description: 'Placed' }) declares one response; the shorthands (@ApiCreatedResponse, @ApiNotFoundResponse, @ApiNoContentResponse, one per status) save the number. Give a response a type and the document references that class's schema, so the class needs @ApiProperty() on its properties like any DTO. An entity can carry both decorators at once, TypeORM's and the document's, and then it is the response model:
@Post()
@ApiCreatedResponse({ description: 'The order as stored', type: Order })
@ApiBadRequestResponse({ description: 'A line has no quantity' })
create(@Body() dto: CreateOrderDto): Promise<Order> {}
A response every operation shares, a 500, a 401, belongs in the builder: addGlobalResponse({ status: 500, description: '...' }) puts it on all of them.
A response the scanner cannot see
A paginated list returns { total, limit, offset, results: T[] }, and results differs per route. A generic class PaginatedDto<T> cannot say what T is at runtime. The docs' answer composes the schema by hand: @ApiExtraModels(PaginatedDto, Order) registers both classes so they appear under components.schemas even though no route references them directly, getSchemaPath(PaginatedDto) returns the $ref string to one, and an allOf joins the page's properties with a results array of the model:
@ApiOkResponse({
schema: {
allOf: [
{ $ref: getSchemaPath(PaginatedDto) },
{ properties: { results: { type: 'array', items: { $ref: getSchemaPath(Order) } } } },
],
},
})
Written once per route that is a lot of decorator. applyDecorators() from @nestjs/common combines several into one, which is how a custom @ApiPaginatedResponse(Order) is made: a function taking the model, returning applyDecorators(ApiExtraModels(...), ApiOkResponse(...)). A title on the schema (PaginatedResponseOfOrder) gives generators a name for the composed type.
Uploads
A file upload is a multipart/form-data body. @ApiConsumes('multipart/form-data') sets the content type, and @ApiBody({ type: UploadDto }) names a DTO whose file property is @ApiProperty({ type: 'string', format: 'binary' }); the Swagger UI turns that into a file picker. The FileInterceptor from the Techniques course still does the receiving.
Your task
The cats API now pages its list (GET /cats?limit=&offset=), returns a PaginatedDto<Cat>, and accepts a photo at POST /cats/:id/photo. Nothing about the operations is documented.
- Describe the
catstag asThe shelter's residentsand declare, for every operation, that a 500 meansSomething broke on the shelter's side. Tag the controllercatsand document theX-Shelter-Siteheader (Which shelter site is calling) on every route. - Make
Cata model: every column visible,breedas the nullableBreedenum,photoan optional nullable string. Give the list, the fetch and the create their summaries (List the cats, a page at a time,Fetch one cat,Register a cat),limitandoffsetas optional numbers, and document the responses: 200 with aCaton the fetch, 201The cat, with its idon the create, 404No cat has that idon the fetch and the delete, 204Removedon the delete. - Finish
ApiPaginatedResponseinapi-paginated-response.decorator.tsso the list's 200 isOne page of resultscomposed as above, and put it on the list route. - Document the upload's body as multipart with
PhotoUploadDto.
When it fails
"photo": { "type": "object", "nullable": true }: astring | nullproperty's metadata isObject; saytype: String.- The 200 of
GET /catsreferencesPaginatedDtobutcomponents.schemashas no such entry:@ApiExtraModelsis missing.getSchemaPathbuilds a$refstring; it registers nothing. - The list's response has
allOfbutresultsis{ "$ref": ... }withouttype: 'array': the composed schema is copied from the docs' JSON example, which shows the shape after the UI resolved it, not the decorator to write. - Every operation is tagged
Cats, capital C:@ApiTagsis missing and the scanner named the tag after the controller.
Remember
@ApiTags,@ApiOperation({ summary })and@ApiHeadersay what an operation is;addTagdescribes the group.@ApiResponseand its status shorthands declare responses, withtypefor a model class;addGlobalResponsefor one every operation shares.- What the scanner cannot see (a generic, a union) is composed by hand:
@ApiExtraModels,getSchemaPath,allOf, packaged withapplyDecorators. - An upload is
@ApiConsumes('multipart/form-data')plus a DTO with a binary-string property.
Stuck? Show a hint
main.ts: .addTag('cats', "The shelter's residents") and .addGlobalResponse({ status: 500, description: '...' }). cat.entity.ts: @ApiProperty() on each column (@ApiPropertyOptional({ enum: Breed, enumName: 'Breed', nullable: true }) for breed, @ApiPropertyOptional({ type: String, nullable: true }) for photo). Controller: @ApiTags('cats') and @ApiHeader({ name: 'X-Shelter-Site', description: '...' }) on the class; @ApiOperation({ summary }) per route; @ApiOkResponse({ type: Cat }), @ApiCreatedResponse({ description, type: Cat }), @ApiNotFoundResponse({ description }), @ApiNoContentResponse({ description }); @ApiQuery({ name, required: false, type: Number }) for limit and offset; @ApiConsumes('multipart/form-data') + @ApiBody({ type: PhotoUploadDto }) on the upload. The decorator: applyDecorators(ApiExtraModels(PaginatedDto, model), ApiOkResponse({ schema: { allOf: [{ $ref: getSchemaPath(PaginatedDto) }, { properties: { results: { type: 'array', items: { $ref: getSchemaPath(model) } } } }] } })).
Press Run tests to start the app. Its log appears here.Graded endpoints
addTag's second argument describes the tag; @ApiTags on the controller puts every operation under it, where the default tag would have been the controller's name
@ApiOperation gives three operations a summary; @ApiHeader on the class adds the X-Shelter-Site header parameter to every operation, before the ones the scanner found
With @ApiProperty on its columns the entity is a schema; the responses of POST /cats and GET /cats/:id reference it, with the shorthand decorators' status and description
@ApiNoContentResponse and @ApiNotFoundResponse document the delete; addGlobalResponse puts the 500 on all five operations without repeating it
The decorator registers PaginatedDto and Cat as extra models and composes the 200 response: PaginatedDto's properties plus results as an array of Cat
@ApiConsumes names the content type and @ApiBody the DTO; its photo property is a string in binary format, which the Swagger UI renders as a file picker
The response is the Cat the document promised, with the columns the entity added
A second row, so the page below has something to skip
The shape the allOf described: total, limit, offset, and results holding Cats
A multipart request with a photo field, as the document describes it; the file name is stored on the cat
@ApiNotFoundResponse described it; NotFoundException produces it
204 with no body, as documented
The page after the delete: total went down with it