VitNode

Data Transfer Object (DTO)

Manipulate data between the client and the server.

Data Transfer Objects (DTOs) are used to define the shape of data that is sent between the client and the server. They are used to validate and transform data before it is passed to the service layer.

Structure DTO in shared folder

VitNode uses a shared folder to store DTOs. This folder is located at apps/shared/plugins/{your_plugin_code}.

Thanks to monorepo structure, you can use this folder in both the frontend and backend. This is useful when you need type-safe data transfer between the client and the server.

Here is an example of how to structure DTOs in the shared folder:

example.dto.ts
categories.dto.ts

Request payloads

As an example, let's create a simple POST endpoint.

apps/backend/src/plugins/{your_plugin_code}/example.controller.ts
import { Controller, Post, Body } from '@nestjs/common';
 
@ApiTags('Welcome')
@Controller('welcome/example')
export class ExampleWelcomeController {
  @Post()
  create() {
    return 'This action adds a new example';
  }
}

But we don't have any methods to provide data to the controller. We have few options to do this:

Body

The @Body() decorator is used to extract the entire body of the request.

Create DTO

Inside the shared folder, create a new file for your DTO.

apps/shared/plugins/{your_plugin_code}/example.dto.ts
import { IsString, IsOptional } from 'class-validator';
import { ApiProperty, ApiPropertyOptional } from '@nestjs/swagger';
 
export class CreateExampleWelcomeBody {
  @ApiProperty()
  @IsString()
  name: string;
 
  @ApiPropertyOptional()
  @IsString()
  @IsOptional()
  description?: string;
}

Import into controller

apps/backend/src/plugins/{your_plugin_code}/example.controller.ts
import { Controller, Post, Body } from '@nestjs/common';
import { CreateExampleWelcomeBody } from 'shared/plugins/{your_plugin_code}/example.dto'; 
 
@ApiTags('Welcome')
@Controller('welcome/example')
export class ExampleWelcomeController {
  @Post()

  create(@Body() body: CreateExampleWelcomeBody) {

    return `This action adds a new example with name: ${body.name} and description: ${body.description}`;
  }
}

Query

The @Query() decorator is used to extract the query parameters from the request.

Example query: http://localhost:8080/welcome/example?name=John&description=Hello

Create DTO

Inside the shared folder, create a new file for your DTO.

apps/shared/plugins/{your_plugin_code}/example.dto.ts
import { IsString, IsOptional } from 'class-validator';
import { ApiProperty, ApiPropertyOptional } from '@nestjs/swagger';
 
export class CreateExampleWelcomeQuery {
  @ApiProperty()
  @IsString()
  name: string;
 
  @ApiPropertyOptional()
  @IsString()
  @IsOptional()
  description?: string;
}

As you can see, the structure is the same as the body DTO. The only difference is that we use the @Query() decorator in the controller.

Import into controller

apps/backend/src/plugins/{your_plugin_code}/example.controller.ts
import { Controller, Post, Query } from '@nestjs/common';
 
import { CreateExampleWelcomeQuery } from 'shared/plugins/{your_plugin_code}/example.dto'; 
 
@ApiTags('Welcome')
@Controller('welcome/example')
export class ExampleWelcomeController {
  @Post()

  create(@Query() query: CreateExampleWelcomeQuery) {

    return `This action adds a new example with name: ${query.name} and description: ${query.description}`;
  }
}

Param

The @Param() decorator is used to extract the route parameters from the request.

Example route: http://localhost:8080/welcome/example/John

Create DTO

Inside the shared folder, create a new file for your DTO.

apps/shared/plugins/{your_plugin_code}/example.dto.ts
import { IsNumberString } from 'class-validator';
import { ApiProperty } from '@nestjs/swagger';
 
export class CreateExampleWelcomeParam {
  @ApiProperty()
  @IsNumberString()
  name: string;
}

The @IsNumberString() decorator is used to validate that the parameter is a number from the client. If the client sends a string, the validation will fail.

Import into controller

apps/backend/src/plugins/{your_plugin_code}/example.controller.ts
import { Controller, Post, Param } from '@nestjs/common';
import { CreateExampleWelcomeParam } from 'shared/plugins/{your_plugin_code}/example.dto'; 
 
@ApiTags('Welcome')
@Controller('welcome/example')
export class ExampleWelcomeController {
  @Post(':name')

  create(@Param() param: CreateExampleWelcomeParam) {

    return `This action adds a new example with name: ${param.name}`;
  }
}

Types and Paremeters

This section is based of NestJS OpenAPI types documentation.

Api Property

The @ApiProperty() and @ApiPropertyOptional() decorators are used to define the metadata for the DTO properties. This metadata is used by the Swagger UI to generate the API documentation. You can provide a description, example, and other parameters.

example.dto.ts
import { IsNumber } from 'class-validator';
import { ApiProperty } from '@nestjs/swagger';
 
export class CreateExampleWelcomeBody {
  @ApiProperty({
    description: 'The age of a cat',
    minimum: 1,
    default: 1,
    example: 1,
  })
  @IsNumber()
  age: number;
}

Arrays

To define an array in a DTO, you can use the Array type or the [] syntax.

example.dto.ts
import { IsArray, IsString } from 'class-validator';
import { ApiProperty } from '@nestjs/swagger';
 
export class CreateExampleWelcomeBody {
  @ApiProperty()
  @IsArray()
  @IsString({ each: true })
  names: string[];
}

Enums

You cannot store an enum in the same file as the DTO. It will cause a issue with the NestJS packages inside shared folder and may not work properly on frontend side.

To use an enum, you need to create a new file in the shared folder.

example.enum.ts
export enum ExampleEnum {
  First = 'First',
  Second = 'Second',
  Third = 'Third',
}

Then you can use it in your DTO.

example.dto.ts
import { IsEnum } from 'class-validator';
import { ApiProperty } from '@nestjs/swagger';
import { ExampleEnum } from 'shared/plugins/{your_plugin_code}/example.enum'; 
 
export class CreateExampleWelcomeBody {
  @ApiProperty({ enum: ExampleEnum, name: 'ExampleEnum' })
  @IsEnum(ExampleEnum)
  name: ExampleEnum;
}

oneOf, anyOf, allOf

To use oneOf, anyOf, or allOf in your DTO, you can use the @ApiProperty() decorator.

example.dto.ts
import { IsString } from 'class-validator';
import { ApiProperty } from '@nestjs/swagger';
 
export class CreateExampleWelcomeBody {
  @ApiProperty({
    oneOf: [{ type: 'string' }, { type: 'number' }],
  })
  value: string | number;
}

Arrays

example.dto.ts
import { IsString } from 'class-validator';
import { ApiProperty } from '@nestjs/swagger';
 
export class CreateExampleWelcomeBody {
  @ApiProperty({
    type: 'array',
    items: {
      oneOf: [{ type: 'string' }, { type: 'number' }],
    },
  })
  values: (string | number)[];
}

Objects

example.dto.ts
import { IsString, IsNumber } from 'class-validator';
import { ApiProperty } from '@nestjs/swagger';
 
class Cat {
  @ApiProperty()
  @IsString()
  name: string;
 
  @ApiProperty()
  @IsNumber()
  age: number;
}
 
class Dog {
  @ApiProperty()
  @IsString()
  breed: string;
 
  @ApiProperty()
  @IsNumber()
  age: number;
}
 
export class CreateExampleWelcomeBody {
  @ApiProperty({
    oneOf: [{ $ref: getSchemaPath(Cat) }, { $ref: getSchemaPath(Dog) }],
  })
  pet: Cat | Dog;
}

Schema Name

If you want to chnge the schema name, you can use the @ApiSchema() decorator.

example.dto.ts
import { ApiSchema } from '@nestjs/swagger';
 
@ApiSchema({ name: 'ExampleWelcome' })
export class CreateExampleWelcomeBody {}

Usage in frontend

You can also use DTOs in the frontend.

apps/frontend/src/plugins/{your_plugin_code}/example.tsx
import { CreateExampleWelcomeBody } from "shared/plugins/{your_plugin_code}/example.dto"; 
 
const ExampleComponent = () => {

  const onSubmit = async (data: CreateExampleWelcomeBody) => {
    ...
  };
};

On this page