Files
What you'll learn
- Put Schema Object keywords (description, example, minimum, default) on a property with @ApiProperty's options, and rename or describe a schema with @ApiSchema
- Document an enum, and give it a schema of its own with enumName so generated clients share one type
- Describe an array property with type: [String], and an array body with @ApiBody, because TypeScript keeps no metadata for either
- Describe query and path parameters beyond what the scanner infers, with @ApiQuery and @ApiParam
Types and parameters
Lesson 1 left the document true but thin. CreateCatDto says name is a string and age is a number, and the volunteer writing the mobile app still has questions: what does a name look like, can age be negative, which breeds are allowed, what is in nicknames? A document that answers those is one nobody has to email you about, and a client generator turns every detail into a type, a bound or a dropdown. Every one of them is a decorator option away, because the scanner only knows what TypeScript's metadata tells it, and metadata stops at the type's name.
Schema Object keywords on a property
@ApiProperty() takes any keyword of an OpenAPI Schema Object. The ones worth writing are the ones a reader cannot infer from the type:
export class CreateBookDto {
@ApiProperty({ example: 'The Left Hand of Darkness', description: 'As printed on the cover' })
title: string;
@ApiProperty({ description: 'Pages, including the index', minimum: 1, default: 200 })
pages: number;
}
example shows up prefilled in the Swagger UI's request editor, description under the field, minimum and default in the schema. Remember that these describe: a minimum: 1 refuses nothing, @Min(1) on the same property does. Keep the two in agreement.
The class itself can be described or renamed with @ApiSchema(). A generator names its classes after schema names, and CreateBookDto is a Nest habit a Java or Swift client does not want; @ApiSchema({ name: 'NewBook', description: '...' }) renames the schema everywhere it is referenced, without touching the class.
Enums
An enum property is a string to the scanner. Give it the values:
@ApiProperty({ enum: Format })
format: Format;
That inlines enum: ['hardback', 'paperback'] into the property. Do it on three DTOs and a generator produces three identical enum types with three names. enumName fixes that by giving the enum a schema of its own under components.schemas, which every property then references:
@ApiProperty({ enum: Format, enumName: 'Format' })
format: Format;
Any decorator that accepts enum accepts enumName, including the parameter decorators below.
Arrays
TypeScript's metadata for titles: string[] is Array, and no more. Say what is in it:
@ApiProperty({ type: [String] })
titles: string[];
The same limit applies to a request body that is an array. @Body() dtos: CreateBookDto[] is scanned as Array; @ApiBody({ type: [CreateBookDto] }) on the handler documents it as an array of that schema. (Validating such a body is ParseArrayPipe's job, from the Techniques course; the pipe and the decorator each do half.)
Query and path parameters
@Query('format') format?: Format is scanned as a required string, because the ? is not metadata and neither is the enum. @ApiQuery() overrides what the scanner inferred, matched by name:
@ApiQuery({ name: 'format', enum: Format, required: false })
findAll(@Query('format') format?: Format) {}
@ApiParam({ name: 'isbn', description: '...' }) does the same for a path parameter: the scanner already knows it exists and is required, and @ApiParam adds the description. Both take type, enum, example and the rest.
Your task
The cats API now has a Breed enum, a nicknames array on the entity, a breed filter on GET /cats and a bulk POST /cats/bulk. The document does not know any of that yet.
- In
create-cat.dto.ts, the schema should be calledNewCatand carry the descriptionWhat the shelter records when a cat arrives. Givenamethe exampleTomand the descriptionWhat the shelter calls the cat; giveagethe descriptionAge in years, a minimum of0and a default of1. - Document
breedas theBreedenum, as a schema of its own with that name, andnicknamesas an array of strings. - In the controller, the breed filter is optional and takes one of the enum's values; the
idofGET /cats/:idhas the descriptionThe cat's id, from the list; the bulk body is an array of the DTO.
Run after step 1 and fetch /api-json: NewCat is there and CreateCatDto is gone, and the request bodies follow.
When it fails
breedis{ "type": "string" }with the values inlined asenum: [...]and there is noBreedschema:enumwas given withoutenumName.nicknamesis{ "type": "array", "items": { "type": "string" } }in the entity but{ "type": "array" }in the DTO:type: [String]is on one and not the other; the document is per class.- The bulk route's body is
{ "type": "array", "items": { "type": "string" } }:@ApiBodyis missing; the scanner sawArrayand guessed. GET /cats?breed=Lionanswers 400Validation failed (enum string is expected)even though the document lists breeds: that is right. The document lists them, theParseEnumPipeenforces them, and a reader who tries a fourth value learns the document was honest.
Remember
@ApiPropertytakes Schema Object keywords:description,example,minimum,default,enum,type.enumNamemakes an enum a shared schema; without it each property repeats the values.- Arrays and array bodies need their item type spelled out:
type: [String],@ApiBody({ type: [Dto] }). @ApiQueryand@ApiParamoverride what the scanner inferred about a parameter, matched by name.
Stuck? Show a hint
create-cat.dto.ts: @ApiSchema({ name: 'NewCat', description: '...' }) on the class; @ApiProperty({ example: 'Tom', description: '...' }), @ApiProperty({ description: 'Age in years', minimum: 0, default: 1 }), @ApiPropertyOptional({ enum: Breed, enumName: 'Breed' }), @ApiProperty({ type: [String] }). cats.controller.ts: @ApiQuery({ name: 'breed', enum: Breed, required: false }) on findAll, @ApiParam({ name: 'id', description: "The cat's id, from the list" }) on findOne, @ApiBody({ type: [CreateCatDto] }) on createMany.
Press Run tests to start the app. Its log appears here.Graded endpoints
@ApiSchema renames CreateCatDto to NewCat in the document and gives it a description; the request body's $ref follows the new name
The options given to @ApiProperty land on the Schema Object: name has an example and a description, age a description, a minimum and a default
enumName turns Breed into a reusable schema listing the three values, and the breed property references it instead of repeating them
type: [String] documents nicknames as an array of strings; TypeScript's metadata only says Array
@ApiQuery makes the breed filter optional with the enum's values; @ApiParam adds a description to the id the scanner already found
@ApiBody({ type: [CreateCatDto] }) documents POST /cats/bulk as an array whose items reference the schema
The API accepts what the document describes
ParseArrayPipe validates each item of the array body
The optional query parameter narrows the list
The document lists three values; ParseEnumPipe is what refuses a fourth
The second item's age is a string; the error names the item that failed
The delete route is unchanged by any of this
The only Siamese is gone