create DTOs, BOs and DAOs for NestJs REST API
Asked Answered
P

1

19

I would like to get into creating REST APIs with NestJs and I'm not sure how to setup scalable layer communication objects.

So from the docs on how to get started I come up with a UsersController dealing with the HTTP requests and responses, a UsersService dealing with the logic between the controller and the database accessor and the UsersRepository which is responsible for the database management.

I use the TypeORM package provided by NestJs so my database model would be

@Entity('User')
export class UserEntity extends BaseEntity {
  @PrimaryGeneratedColumn('uuid')
  id: string;

  @Column({ unique: true })
  username: string;

  @Column()
  passwordHash: string;

  @Column()
  passwordSalt: string;
}

but as you might know this model has to be mapped to other models and vice versa because you don't want to send the password information back to the client. I will try to describe my API flow with a simple example:


Controllers

First I have a controller endpoint for GET /users/:id and POST /users.

  @Get(':id')
  findById(@Param() findByIdParamsDTO: FindByIdParamsDTO): Promise<UserDTO> {
    // find user by id and return it
  }

  @Post()
  create(@Body() createUserBodyDTO: CreateUserBodyDTO): Promise<UserDTO> {
    // create a new user and return it
  }

I setup the DTOs and want to validate the request first. I use the class-validator package provided by NestJs and created a folder called RequestDTOs. Finding something by id or deleting something by id via url parameters is reusable so I can put this into a shared folder for other resources like groups, documents, etc.

export class IdParamsDTO {
  @IsUUID()
  id: string;
}

The POST request is user specific

export class CreateUserBodyDTO {
  @IsString()
  @IsNotEmpty()
  username: string;

  @IsString()
  @IsNotEmpty()
  password: string;
}

Now the controller input gets validated before executing business logic. For the responses I created a folder called ResponseDTOs but currently it only contains the database user without its password information

export interface UserDTO {
  id: string;
  username: string;
}

Services

The service needs the bundled information from the params and the body.

  public async findById(findByIdBO: FindByIdBO): Promise<UserBO> {
    // ...
  }

  public async create(createBO: CreateBO): Promise<UserBO> {
    // ...
  }

The GET request only needs the ID, but maybe it's still better to create a BO because you might want to switch from string IDs to integers later. The "find by id" BO is reusable, I moved it to the shared directory

export interface IdBO {
  id: string;
}

For the user creation I created the folder RequestBOs

export interface CreateBO {
  username: string;
  password: string;
}

Now for the ResponseBOs the result would be

export interface UserBO {
  id: string;
  username: string;
}

and as you will notice this is the same like the UserDTO. So one of them seems to be redundant?


Repositories

Lastly I setup the DAOs for the repositories. I could use the auto-generated user repository and would deal with my database model I mentioned above. But then I would have to deal with it within my service business logic. When creating a user I would have to do it within the service and only call the usermodel.save function from the repository.

Otherwise I could create RequestDAOs

The shared one..

export interface IdDAO {
  id: string;
}

And the POST DAO

export interface CreateDAO {
  username: string;
  password: string;
}

With that I could create a database user within my repository and map database responses with ResponseDAOs but this would always be the whole database user without the password information. Seems to generate a big overhead again.


I would like to know if my approach using 3 request and 3 response interfaces is way too much and can be simplified. But I would like to keep a flexible layer because I think those layers should be highly independent... On the other hand there would be a huge amount of models out there.

Thanks in advance!

Policeman answered 31/12, 2019 at 18:1 Comment(2)
Honestly I believe the 3 request/response dto's is the way to go and here's why: In theory if you had a "UsersModule", that module would return "User" models to the rest of the application BUT how that module talks to the database should be no concern to the rest of the application. It would define it's own dto's for communication to the database. That way if you decide to swap out what database users get stored in, the rest of the application remains unaffected. This creates the correct separation of concerns and is a good pattern despite the "duplication" of models/dto's.Garnes
hm yes, I was just thinking about it because I only can image a user where I need to hide the sensitive data (password). Groups for example could be returned as database models ...Policeman
T
33

I handle this by having a single class to represent a User (internally and externally) with the class-transformer library (recommended by NestJs) to handle the differences between the exposed user and the internal user without defining two classes.

Here's an example using your user model:

Defining the User Class

Since this user class is saved to the database, I usually create a base class for all the fields that every database object expects to have. Let's say:

export class BaseDBObject {
  // this will expose the _id field as a string
  // and will change the attribute name to `id`
  @Expose({ name: 'id' })
  @Transform(value => value && value.toString())
  @IsOptional()
  // tslint:disable-next-line: variable-name
  _id: any;

  @Exclude()
  @IsOptional()
  // tslint:disable-next-line: variable-name
  _v: any;

  toJSON() {
    return classToPlain(this);
  }

  toString() {
    return JSON.stringify(this.toJSON());
  }
}

Next, our user will expend this basic class:

@Exclude()
export class User extends BaseDBObject {
  @Expose()
  username: string;

  password: string;

  constructor(partial: Partial<User> = {}) {
    super();
    Object.assign(this, partial);
  }
}

I'm using a few decorators here from the class-transformer library to change this internal user (with all the database fields intact) when we expose the class outside of our server.

  • @Expose - will expose the attribute if the class-default is to exclude
  • @Exclude - will exclude the property if the class-default is to expose
  • @Transform - changes the attribute name when 'exporting'

This means that after running the classToPlain function from class-transformer, all the rules we defined on the given class will be applied.

Controllers

NestJs have a decorator you add to make sure classes you return from controller endpoints will use the classToPlain function to transform the object, returning the result object with all the private fields omitted and transformations (like changing _id to id)

@Get(':id')
@UseInterceptors(ClassSerializerInterceptor)
async findById(@Param('id') id: string): Promise<User> {
  return await this.usersService.find(id);
}

@Post()
@UseInterceptors(ClassSerializerInterceptor)
async create(@Body() createUserBody: CreateUserBodyDTO): Promise<User> {
  // create a new user from the createUserDto
  const userToCreate = new User(createUserBody);

  return await this.usersService.create(userToCreate);
}

Services

@Injectable()
export class UsersService {
  constructor(@InjectModel('User') private readonly userModel: Model<IUser>) { }

  async create(createCatDto: User): Promise<User> {
    const userToCreate = new User(createCatDto);
    const createdUser = await this.userModel.create(userToCreate);

    if (createdUser) {
      return new User(createdUser.toJSON());
    }
  }

  async findAll(): Promise<User[]> {
    const allUsers = await this.userModel.find().exec();
    return allUsers.map((user) => new User(user.toJSON()));
  }

  async find(_id: string): Promise<User> {
    const foundUser = await this.userModel.findOne({ _id }).exec();
    if (foundUser) {
      return new User(foundUser.toJSON());
    }
  }
}

Because internally we always use the User class, I convert the data returned from the database to a User class instance.

I'm using @nestjs/mongoose, but basically after retrieving the user from the db, everything is the same for both mongoose and TypeORM.

Caveats

With @nestjs/mongoose, I can't avoid creating IUser interface to pass to the mongo Model class since it expects something that extends the mongodb Document

export interface IUser extends mongoose.Document {
  username: string;

  password: string;
}

When GETting a user, the API will return this transformed JSON:

{
    "id": "5e1452f93794e82db588898e",
    "username": "username"
}

Here's the code for this example in a GitHub repository.

Update

If you want to see an example using typegoose to eliminate the interface as well (based on this blog post), take a look here for a model, and here for the base model

Topgallant answered 7/1, 2020 at 10:10 Comment(11)
If this is close to what you're trying to achieve, let me know and I'll try and add an example on how to do this with TypeORM as wellTopgallant
Great answer. However for the interface part, I would recommend using something like typegoose to not involve mongoose.Document interface here.Mommy
very nice answer @ thatkookooguy :) It would be awesome if you could expand your answer and add a TypeORM examplePoliceman
as far as I understood the sensitive data gets cut off while converting the User model to the IUser interface? while running your interceptor?Policeman
@ChauTran wow. that's like the missing link to make this all work perfectly! thank you for opening my eyes to this :-)Topgallant
@Policeman sure. I'll try and provide an answer with typeORM soon :-). Basically, the class you define is the internal class, while everything sensitive is cut off when either you use the function classToPlain from class-transformer or by using the nestjs ClassSerializerInterceptor which calls classToPlain behind the scenesTopgallant
@Topgallant total off topic here but here's a blog post I wrote some times ago to demonstrate the use of Typegoose in NestJS: nartc.netlify.com/blogs/nestjs-typegooseMommy
nice architecture, quick question : Do we really need to call user.toJSON to transform the response, I've tried without it and it is working fine(transformation).Knuckleduster
I believe your controller code will throw an error where you are trying to create an instance of User by supplying createUserBody. User constructor expect Partial<User> as a parameter.Knuckleduster
@Knuckleduster You're probably right. I added the toJSON function mostly for having the ability to do things manually whenever needed (keeping some attributes from leaving a certain function). In normal controller transformations, it's probably not needed. About the error, I'll test it and check. I already changed the code in my codebase quite a bit and worked with it for a couple of months, so maybe something was fixed or got missed. I'll check the code snippet soonish and fix the problem or update the code. Thanks for the comments!Topgallant
@Topgallant appreciate your response. I'm new to this nodejs (nestjs + typorm ). Having some difficulties finding the best approach to design the entire framework. More related to dependency injection (should be inject multiple repositories inside one service or inject multiple services inside one service and resolve those dependencies at module level) . I can explain this in details but we need a platform to discuss them, could you help ?Knuckleduster

© 2022 - 2024 — McMap. All rights reserved.