InteractiveFrameworks

Types and parameters

Make the document say what the API really accepts: examples, bounds and defaults on each property, an enum with a schema of its own, an optional query parameter with allowed values, a described path parameter, and a request body that is an array of DTOs.

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

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.

  1. In create-cat.dto.ts, the schema should be called NewCat and carry the description What the shelter records when a cat arrives. Give name the example Tom and the description What the shelter calls the cat; give age the description Age in years, a minimum of 0 and a default of 1.
  2. Document breed as the Breed enum, as a schema of its own with that name, and nicknames as an array of strings.
  3. In the controller, the breed filter is optional and takes one of the enum's values; the id of GET /cats/:id has the description The 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

  • breed is { "type": "string" } with the values inlined as enum: [...] and there is no Breed schema: enum was given without enumName.
  • nicknames is { "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" } }: @ApiBody is missing; the scanner saw Array and guessed.
  • GET /cats?breed=Lion answers 400 Validation failed (enum string is expected) even though the document lists breeds: that is right. The document lists them, the ParseEnumPipe enforces them, and a reader who tries a fourth value learns the document was honest.

Remember

  • @ApiProperty takes Schema Object keywords: description, example, minimum, default, enum, type.
  • enumName makes 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] }).
  • @ApiQuery and @ApiParam override 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.