为 NestJs REST API 创建 DTO、BO 和 DAO

2023-12-31

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

所以从关于如何的文档开始吧 https://docs.nestjs.com/controllers我想出了一个UsersController处理 HTTP 请求和响应,UsersService处理控制器和数据库访问器之间的逻辑UsersRepository它负责数据库管理。

我用类型ORM包 https://docs.nestjs.com/recipes/sql-typeorm由 NestJs 提供,所以我的数据库模型是

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

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

  @Column()
  passwordHash: string;

  @Column()
  passwordSalt: string;
}

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


控制器

首先我有一个控制器端点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
  }

我设置了DTOs https://en.wikipedia.org/wiki/Data_transfer_object并想首先验证请求。我用类验证器 https://docs.nestjs.com/techniques/validationNestJs 提供的包并创建了一个名为的文件夹请求DTO。通过 id 查找某些内容或通过 url 参数按 id 删除某些内容是可重用的,因此我可以将其放入共享文件夹中以存放其他资源(如组、文档等)。

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

POST 请求是用户特定的

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

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

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

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

Services

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

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

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

GET 请求只需要 ID,但也许创建一个更好BO https://en.wikipedia.org/wiki/Business_object因为稍后您可能想从字符串 ID 切换到整数。 “find by id”BO是可重用的,我将其移至共享目录

export interface IdBO {
  id: string;
}

为了创建用户,我创建了文件夹请求BO

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

现在对于响应BO结果将是

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

你会注意到这和UserDTO。那么其中之一似乎是多余的?


存储库

最后我设置了DAOs https://en.wikipedia.org/wiki/Data_access_object对于存储库。我可以使用自动生成的用户存储库,并处理我上面提到的数据库模型。但随后我必须在我的服务业务逻辑中处理它。创建用户时,我必须在服务中执行此操作,并且仅调用usermodel.save来自存储库的函数。

否则我可以创建请求DAO

共享的那一个..

export interface IdDAO {
  id: string;
}

还有 POST DAO

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

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


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

提前致谢!


我通过使用一个类来代表用户(内部和外部)来处理这个问题class-transformer https://www.npmjs.com/package/class-transformer库(NestJs 推荐)用于处理公开用户和内部用户之间的差异,而无需定义两个类。

这是使用您的用户模型的示例:

定义用户类别

由于该用户类保存到数据库中,因此我通常为每个数据库对象期望拥有的所有字段创建一个基类。比方说:

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());
  }
}

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

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

  password: string;

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

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

  • @Expose- 如果类默认要排除,则将公开该属性
  • @Exclude- 如果类默认要公开,则将排除该属性
  • @Transform-“导出”时更改属性名称

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

控制器

NestJs添加一个装饰器以确保从控制器端点返回的类将使用classToPlain函数来转换对象,返回结果对象,并省略所有私有字段和转换(例如更改_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());
    }
  }
}

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

我在用着@nestjs/mongoose,但基本上从数据库检索用户后,两者的一切都是相同的mongoose and TypeORM.

Caveats

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

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

  password: string;
}

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

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

以下是 GitHub 存储库中此示例的代码 https://github.com/Thatkookooguy/once-class-demo.

Update

如果您想查看使用的示例typegoose也消除了接口(基于这篇博文 https://nartc.netlify.com/blogs/nestjs-typegoose/), 看一看这里有一个模型 https://github.com/Kibibit/achievibit/blob/beta/server/src/models/user.model.ts, and 这里是基本模型 https://github.com/Kibibit/achievibit/blob/beta/server/src/abstracts/base.model.abstract.ts

本文内容由网友自发贡献,版权归原作者所有,本站不承担相应法律责任。如您发现有涉嫌抄袭侵权的内容,请联系:hwhale#tublm.com(使用前将#替换为@)

为 NestJs REST API 创建 DTO、BO 和 DAO 的相关文章

随机推荐