Search code examples
typescriptnestjs

What is the best practice about DTO, schema or interface for typing in typescript


Currently in my typescript code (nestjs) I use the DTO in my controller to validate the data that goes into my API, the schemas are used as type in the rest of the files and I do not create an interface except in special cases.

I'm trying to figure out if what I'm doing is good or if I should use the DTO as a type everywhere, or something? The goal is to increase the quality of my code.

Example with the user collection:

user.schema.ts

import { Prop, Schema, SchemaFactory } from '@nestjs/mongoose';
import { Document } from 'mongoose';

@Schema({ collection: 'users' })
export class User extends Document {
  @Prop()
  name: string;
}

export const UserSchema = SchemaFactory.createForClass(User);
UserSchema.set('timestamps', true);

user.dto.ts

import { IsNotEmpty, IsString, Length } from 'class-validator';
import { ApiProperty } from '@nestjs/swagger';

export class UserDto {
  @IsNotEmpty()
  @IsString()
  @Length(3)
  @ApiProperty({ required: true })
  readonly name: string;
}

Example of use :

  • in a controller with the dto
async createUser(@Body() user: UserDto): Promise<UserSchema> {
  const nameAlreadyExist = await this.userService.getUserByField('name', user.name);
  if (nameAlreadyExist && nameAlreadyExist.length > 0) {
    throw new ConflictException(getErrorObject(ErrorCodeEnum.duplicate, EntityCodeEnum.user, ['name']));
  }
  return this.userService.createUser(user as UserSchema);
}
  • in the service with the schema
async createUser(user: UserSchema): Promise<UserSchema> {
  const newUser = new this.UserModel(user);
  return newUser.save();
}

Solution

  • The DTO pattern is useful in the context of NestJS because when you use actual classes you can apply decorators to be able to validate requests at your API boundary.

    Schemas are a Mongo concept and are required in order to enable reading and writing from the database. If there is complete overlap between the Schema and the DTO you could consider merging them into a single class and combining their decorators. However, it is generally considered good practice to have a separate non DB representation of your models which is handled at the API layer.

    Interfaces are a general TypeScript language feature for being able to describe the shape of data. They are very useful inside library code when you want to enforce compile time safety inside of code that you control. Once you are inside the service layer if you don't require decorators you can always reach for Interfaces or Types to promote type saftey.

    There is no single right answer to your question. It will always depend on the needs of your application but in summary I think a good structure to follow is:

    • DTOs at the API boundary to validate messages coming from other systems
    • Schema/DB Models in your service layer to enable database interactions separate from your controllers
    • Interfaces/Types for all internal lib code that doesn't require decorators