InteractiveFrameworks

Mapped types and other features

Derive the update, birthday and intake DTOs from CreateCatDto with the mapped types from @nestjs/swagger, so the document and the validation follow one definition; keep an internal column and a staff-only route out of the document; and serve the shelter's internal routes as a second specification of their own.

What you'll learn

  • Derive DTOs with PartialType, PickType, OmitType and IntersectionType from @nestjs/swagger, which carry both the document's and class-validator's metadata, and compose them
  • Keep a property out of a schema with @ApiHideProperty and a route out of the document with @ApiExcludeEndpoint (or a controller with @ApiExcludeController), while the app keeps serving them
  • Serve more than one specification from one app with createDocument's include option and a second setup path
  • Say what the Nest CLI's Swagger plugin would add at build time, and why this browser, like ts-jest or plain tsc, compiles without it

CreateCatDto has four properties and eight decorators. The update route wants the same four, all optional; the birthday route wants just the age; the intake route wants everything but the nicknames, plus where the stray was found. Written by hand that is three more classes repeating the same decorators, and the day age gains a maximum, one of them will be missed. The Techniques course met this problem for validation and solved it with the mapped types from @nestjs/mapped-types. @nestjs/swagger exports the same four functions, and its versions carry both kinds of metadata: the document's and class-validator's.

Four functions

import { IntersectionType, OmitType, PartialType, PickType } from '@nestjs/swagger';

export class UpdateBookDto extends PartialType(CreateBookDto) {}
export class UpdateBookPagesDto extends PickType(CreateBookDto, ['pages'] as const) {}
export class BookSummaryDto extends OmitType(CreateBookDto, ['isbn'] as const) {}
export class ImportBookDto extends IntersectionType(CreateBookDto, ProvenanceDto) {}

PartialType keeps every property and makes each optional: no required list in the schema, @IsOptional() in front of every validator. PickType keeps the listed properties, OmitType all but them, and IntersectionType joins two classes. They compose, so PartialType(OmitType(CreateBookDto, ['isbn'] as const)) is a class too. The as const is for TypeScript, so the property names stay literal types and a misspelled one is a compile error. Import from @nestjs/swagger, not @nestjs/mapped-types: the latter's functions copy only the validation metadata, and a DTO derived with them is an empty schema.

Leaving things out

A document should describe what a caller may use, and an app has more than that. @ApiHideProperty() on a property keeps it out of the class's schema; on an entity it pairs with TypeORM's select: false, so the column is neither returned nor advertised. @ApiExcludeEndpoint() on a handler keeps that operation out of the document, and @ApiExcludeController() does it for a whole controller. None of them changes what the app serves. A hidden route still answers, which is the point: staff tooling and the partner's generated client read the same server through different descriptions.

Mind what save() returns. TypeORM answers a save with the entity it wrote, every column of it, hidden or not; a route that returns that object leaks the column the document hides. Read the row back after writing, and the response is what a reader was promised.

More than one specification

createDocument() takes an include list of modules, and scans only those. Two calls with two builders and two setup() paths give two documents from one app, the docs' cats-and-dogs example:

const publicDocument = () => SwaggerModule.createDocument(app, publicConfig, { include: [BooksModule] });
SwaggerModule.setup('api', app, publicDocument);

const adminDocument = () => SwaggerModule.createDocument(app, adminConfig, { include: [AdminModule] });
SwaggerModule.setup('api/admin', app, adminDocument);

Each path serves its own UI page and its own -json. include scans the listed modules' own controllers; deepScanRoutes: true follows their imports too. Related: addGlobalParameters() on the builder for a header every operation takes, and, under app.setGlobalPrefix(), ignoreGlobalPrefix in the document options and useGlobalPrefix in the setup options.

What the browser does not run

The Nest CLI can load @nestjs/swagger's compiler plugin, which reads your .dto.ts and .entity.ts files at build time and adds @ApiProperty() for you: required from the ?, arrays from [], enums, defaults, and with introspectComments the descriptions from your comments. It is a TypeScript transformer, so it runs only where the CLI's compiler runs. This workspace compiles the way ts-jest or a plain tsc does, without it, which is why every lesson wrote the decorators by hand, and why a project that uses the plugin still needs them in its e2e tests unless the transformer is wired into Jest as well. What you wrote here is what the plugin would have generated.

Your task

The API has an intake route and a birthday route, a foundAt column, an internalNote column that is never selected, a staff-only POST /cats/:id/internal-note, and an InternalModule with health and stats routes. The three new DTOs are hand-copied, and one document describes everything.

  1. Derive UpdateCatDto, UpdateCatAgeDto and IntakeDto from CreateCatDto (and IntakeNotesDto) with the mapped types. The document and the validation must not change: run the tests after replacing each one.
  2. Keep internalNote out of the Cat schema, and the internal-note route out of the document. Both keep working.
  3. Make the document at api describe the cats module only, and serve a second document, Shelter internals version 1.0, at api/internal, describing the internal module only.

When it fails

  • UpdateCatDto is { "type": "object", "properties": {} } though the class extends PartialType(CreateCatDto): the import is from @nestjs/mapped-types. That version copies validators, not the document's metadata.
  • PATCH /cats/1 with { "age": "four" } answers 200 and stores the string: the derived class was written as a TypeScript type (Partial<CreateCatDto>), which is erased at compile time; a mapped type is a runtime class.
  • POST /cats answers with "internalNote": null: save() returned what it wrote. Return a read.
  • /api/internal-json lists the cats routes too: the second createDocument() has no include, so it scanned the whole app.

Remember

  • PartialType, PickType, OmitType, IntersectionType from @nestjs/swagger carry both the document's and the validators' metadata; they compose.
  • @ApiHideProperty, @ApiExcludeEndpoint, @ApiExcludeController shape the document, never the app.
  • include in the document options, and one setup() per path, give one app several specifications.
  • The CLI plugin generates @ApiProperty at build time; without it, here as under ts-jest, you write them.
Stuck? Show a hint

update-cat.dto.ts: class UpdateCatDto extends PartialType(CreateCatDto) {}. update-cat-age.dto.ts: PickType(CreateCatDto, ['age'] as const). intake.dto.ts: IntersectionType(OmitType(CreateCatDto, ['nicknames'] as const), IntakeNotesDto). All four from '@nestjs/swagger'. cat.entity.ts: @ApiHideProperty() instead of @ApiPropertyOptional on internalNote. cats.controller.ts: @ApiExcludeEndpoint() on setInternalNote. main.ts: createDocument(app, config, { include: [CatsModule], ... }) for the public document, then a second DocumentBuilder (title 'Shelter internals', version '1.0') and SwaggerModule.setup('api/internal', app, () => SwaggerModule.createDocument(app, internalConfig, { include: [InternalModule] })).