为 NestJs REST API 创建 DTO、BO 和 DAO [英] create DTOs, BOs and DAOs for NestJs REST API

查看:245
本文介绍了为 NestJs REST API 创建 DTO、BO 和 DAO的处理方法,对大家解决问题具有一定的参考价值,需要的朋友们下面随着小编来一起学习吧!

问题描述

我想开始使用 NestJs 创建 REST API,但我不确定如何设置可扩展的层通信对象.

所以从关于如何开始的文档中,我想出了一个UsersController处理HTTP请求和响应,UsersService处理控制器和数据库访问器之间的逻辑,UsersRepository负责数据库管理.>

我使用 NestJs 提供的 TypeORM 包,所以我的数据库模型是>

@Entity('用户')导出类 UserEntity 扩展 BaseEntity {@PrimaryGeneratedColumn('uuid')id:字符串;@Column({ 唯一: 真})用户名:字符串;@柱子()密码哈希:字符串;@柱子()密码盐:字符串;}

但正如您可能知道的那样,此模型必须映射到其他模型,反之亦然,因为您不想将密码信息发送回客户端.我将尝试用一个简单的例子来描述我的 API 流程:

<小时>

控制器

首先,我有一个用于 GET/users/:idPOST/users 的控制器端点.

 @Get(':id')findById(@Param() findByIdParamsDTO: FindByIdParamsDTO): Promise{//通过 id 查找用户并返回}@邮政()create(@Body() createUserBodyDTO: CreateUserBodyDTO): Promise{//创建一个新用户并返回}

我设置了 DTO 并想先验证请求.我使用 NestJs 提供的 class-validator 包并创建了一个名为 RequestDTOs.通过 url 参数通过 id 查找某些内容或通过 id 删除某些内容是可重用的,因此我可以将其放入共享文件夹中以供其他资源(如群组、文档等)使用.

导出类 IdParamsDTO {@IsUUID()id:字符串;}

POST 请求是用户特定的

导出类 CreateUserBodyDTO {@IsString()@IsNotEmpty()用户名:字符串;@IsString()@IsNotEmpty()密码:字符串;}

现在控制器输入在执行业务逻辑之前得到验证.对于响应,我创建了一个名为 ResponseDTOs 的文件夹,但目前它只包含没有密码信息的数据库用户

导出接口 UserDTO {id:字符串;用户名:字符串;}

<小时>

服务

服务需要来自参数和正文的捆绑信息.

 public async findById(findByIdBO: FindByIdBO): Promise{//...}公共异步创建(createBO:CreateBO):承诺{//...}

GET 请求只需要 ID,但也许创建一个 BO 更好,因为您可能希望稍后从字符串 ID 切换到整数.按id查找"BO可复用,我移到共享目录

导出接口 IdBO {id:字符串;}

为了创建用户,我创建了文件夹 RequestBOs

导出接口 CreateBO {用户名:字符串;密码:字符串;}

现在对于 ResponseBO 来说,结果是

导出接口 UserBO {id:字符串;用户名:字符串;}

您会注意到这与 UserDTO 相同.所以其中一个似乎是多余的?

<小时>

存储库

最后,我为存储库设置了 DAO.我可以使用自动生成的用户存储库并处理我上面提到的数据库模型.但随后我将不得不在我的服务业务逻辑中处理它.创建用户时,我必须在服务中执行此操作,并且只能从存储库中调用 usermodel.save 函数.

否则我可以创建RequestDAO

共享的..

导出接口 IdDAO {id:字符串;}

和 POST DAO

导出接口 CreateDAO {用户名:字符串;密码:字符串;}

这样我就可以在我的存储库中创建一个数据库用户并使用 ResponseDAOs 映射数据库响应,但这将始终是没有密码信息的整个数据库用户.似乎又产生了很大的开销.

<小时>

我想知道我使用 3 个请求和 3 个响应接口的方法是否太多并且可以简化.但我想保留一个灵活的层,因为我认为这些层应该是高度独立的……另一方面,那里会有大量的模型.

提前致谢!

解决方案

我通过使用 class-transformer 库(NestJs 推荐)处理暴露用户和内部用户之间的差异,无需定义两个类.

以下是使用您的用户模型的示例:

定义用户类

由于这个用户类被保存到数据库中,我通常为每个数据库对象期望拥有的所有字段创建一个基类.让我们说:

导出类 BaseDBObject {//这会将 _id 字段公开为字符串//并将属性名称更改为 `id`@Expose({ name: 'id' })@Transform(value => value && value.toString())@IsOptional()//tslint:disable-next-line: 变量名_id:任何;@排除()@IsOptional()//tslint:disable-next-line: 变量名_v:任何;toJSON() {返回 classToPlain(this);}toString() {返回 JSON.stringify(this.toJSON());}}

接下来,我们的用户将使用这个基本类:

@Exclude()导出类用户扩展 BaseDBObject {@暴露()用户名:字符串;密码:字符串;构造函数(部分:部分<用户> = {}){极好的();Object.assign(this, partial);}}

当我们在服务器外部公开类时,我在这里使用了 class-transformer 库中的一些装饰器来更改这个内部用户(所有数据库字段都完好无损).

  • @Expose - 如果 class-default 是排除,将公开属性
  • @Exclude - 如果 class-default 是公开的,将排除该属性
  • @Transform - 在导出"时更改属性名称

这意味着在从 class-transformer 运行 classToPlain 函数后,我们在给定类上定义的所有规则都将被应用.

控制器

NestJs 有一个你添加的装饰器,以确保你从控制器端点返回的类将使用 classToPlain 函数来转换对象,返回带有所有私有属性的结果对象省略的字段和转换(例如将 _id 更改为 id)

@Get(':id')@UseInterceptors(ClassSerializerInterceptor)async findById(@Param('id') id: string): Promise{返回等待 this.usersService.find(id);}@邮政()@UseInterceptors(ClassSerializerInterceptor)async create(@Body() createUserBody: CreateUserBodyDTO): Promise{//从 createUserDto 创建一个新用户const userToCreate = 新用户(createUserBody);return await this.usersService.create(userToCreate);}

服务

@Injectable()导出类 UsersService {构造函数(@InjectModel('User') private readonly userModel: Model) { }异步创建(createCatDto:用户):承诺<用户>{const userToCreate = 新用户(createCatDto);const createdUser = await this.userModel.create(userToCreate);如果(创建用户){返回新用户(createdUser.toJSON());}}异步 findAll(): Promise{const allUsers = await this.userModel.find().exec();return allUsers.map((user) => new User(user.toJSON()));}async find(_id: string): Promise{const foundUser = await this.userModel.findOne({ _id }).exec();如果(发现用户){返回新用户(foundUser.toJSON());}}}

因为在内部我们总是使用 User 类,所以我将从数据库返回的数据转换为 User 类实例.

我正在使用 @nestjs/mongoose,但基本上在从数据库中检索用户后,mongooseTypeORM.

注意事项

使用 @nestjs/mongoose,我无法避免创建 IUser 接口来传递给 mongo Model 类,因为它需要一些东西扩展 mongodb Document

export interface IUser extends mongoose.Document {用户名:字符串;密码:字符串;}

当获取用户时,API 将返回这个转换后的 JSON:

<代码>{id":5e1452f93794e82db588898e",用户名":用户名"}

这是 GitHub 存储库中此示例的代码.

希望这有帮助!

更新

如果您还想查看使用 typegoose 消除界面的示例(基于 这篇博文),看看此处为模型,以及此处为基本模型

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!

解决方案

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.

Hope this helped!

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

这篇关于为 NestJs REST API 创建 DTO、BO 和 DAO的文章就介绍到这了,希望我们推荐的答案对大家有所帮助,也希望大家多多支持IT屋!

查看全文
登录 关闭
扫码关注1秒登录
发送“验证码”获取 | 15天全站免登陆