为 NestJs REST API 创建 DTO、BO 和 DAO
Posted
技术标签:
【中文标题】为 NestJs REST API 创建 DTO、BO 和 DAO【英文标题】:create DTOs, BOs and DAOs for NestJs REST API 【发布时间】:2020-04-20 04:02:27 【问题描述】:我想开始使用 NestJs 创建 REST API,但我不确定如何设置可扩展层通信对象。
所以从关于如何get started 的文档中,我想出了一个UsersController
处理HTTP 请求和响应,一个UsersService
处理控制器和数据库访问器之间的逻辑以及UsersRepository
负责数据库管理。
我使用 NestJs 提供的TypeORM package,所以我的数据库模型是
@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
和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 并想先验证请求。我使用 NestJs 提供的 class-validator 包并创建了一个名为 RequestDTOs 的文件夹。通过 id 查找某些内容或通过 url 参数通过 id 删除某些内容是可重复使用的,因此我可以将其放入共享文件夹中,用于其他资源(如组、文档等)。
export class IdParamsDTO
@IsUUID()
id: string;
POST 请求是用户特定的
export class CreateUserBodyDTO
@IsString()
@IsNotEmpty()
username: string;
@IsString()
@IsNotEmpty()
password: string;
现在控制器输入在执行业务逻辑之前得到验证。对于回复,我创建了一个名为 ResponseDTOs 的文件夹,但目前它只包含没有密码信息的数据库用户
export interface UserDTO
id: string;
username: string;
服务
服务需要来自参数和正文的捆绑信息。
public async findById(findByIdBO: FindByIdBO): Promise<UserBO>
// ...
public async create(createBO: CreateBO): Promise<UserBO>
// ...
GET 请求只需要 ID,但创建 BO 可能会更好,因为您可能希望稍后从字符串 ID 切换为整数。 “按 id 查找”BO 是可重复使用的,我将其移至共享目录
export interface IdBO
id: string;
为了创建用户,我创建了文件夹 RequestBOs
export interface CreateBO
username: string;
password: string;
现在对于 ResponseBOs,结果将是
export interface UserBO
id: string;
username: string;
您会注意到这与 UserDTO 相同。所以其中一个似乎是多余的?
存储库
最后我为存储库设置了DAOs。我可以使用自动生成的用户存储库并处理我上面提到的数据库模型。但是我必须在我的服务业务逻辑中处理它。创建用户时,我必须在服务中执行此操作,并且只能从存储库中调用 usermodel.save
函数。
否则我可以创建 RequestDAO
共享的..
export interface IdDAO
id: string;
还有 POST DAO
export interface CreateDAO
username: string;
password: string;
这样我可以在我的存储库中创建一个数据库用户并使用 ResponseDAOs 映射数据库响应,但这始终是没有密码信息的整个数据库用户。似乎又产生了很大的开销。
我想知道我使用 3 个请求和 3 个响应接口的方法是否太多并且可以简化。但我想保留一个灵活的层,因为我认为这些层应该是高度独立的......另一方面,那里会有大量的模型。
提前致谢!
【问题讨论】:
老实说,我相信 3 个请求/响应 dto 是可行的方法,原因如下:理论上,如果您有一个“UsersModule”,该模块会将“用户”模型返回给应用程序的其余部分但是该模块如何与数据库对话应该与应用程序的其余部分无关。它将定义它自己的 dto 用于与数据库的通信。这样,如果您决定交换存储用户的数据库,应用程序的其余部分将不受影响。这创建了正确的关注点分离,并且是一个很好的模式,尽管模型/dto 有“重复”。 嗯,是的,我只是在考虑它,因为我只能在需要隐藏敏感数据(密码)的地方对用户进行成像。例如,组可以作为数据库模型返回... 【参考方案1】:我通过使用 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
- 如果 class-default 要公开,则将排除该属性
@Transform
- 在“导出”时更改属性名称
这意味着在从class-transformer
运行classToPlain
函数后,我们在给定类上定义的所有规则都将被应用。
控制器
NestJs
有一个你添加的装饰器,以确保你从控制器端点返回的类将使用 classToPlain
函数来转换对象,返回所有私有字段省略和转换的结果对象(如更改 _id
给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);
服务
@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
和TypeORM
的一切都相同。
注意事项
使用@nestjs/mongoose
,我无法避免创建IUser
接口以传递给mongo Model
类,因为它需要扩展mongodb Document
的东西
export interface IUser extends mongoose.Document
username: string;
password: string;
获取用户时,API 将返回这个转换后的 JSON:
"id": "5e1452f93794e82db588898e",
"username": "username"
Here's the code for this example in a GitHub repository.
更新
如果您想查看使用typegoose
来消除接口的示例(基于this blog post),请查看here for a model 和here for the base model
【讨论】:
很好的答案。但是对于interface
部分,我建议使用typegoose
之类的东西,而不是在这里涉及mongoose.Document
接口。
非常好的答案@thatkookooguy :) 如果您可以扩展您的答案并添加一个 TypeORM 示例,那就太棒了
据我了解,在将 User 模型转换为 IUser 界面时敏感数据会被截断?在运行拦截器时?
@Thatkookooguy 在这里完全偏离主题,但这是我前段时间写的一篇博文,用于演示在 NestJS 中使用 Typegoose:nartc.netlify.com/blogs/nestjs-typegoose
不错的架构,快速提问:我们真的需要调用 user.toJSON 来转换响应吗,我试过没有它,它工作正常(转换)。以上是关于为 NestJs REST API 创建 DTO、BO 和 DAO的主要内容,如果未能解决你的问题,请参考以下文章
Spring boot rest api - 我可以在不为响应对象创建任何 java 类(DTO 或实体)的情况下获得响应吗?
NestJS-Prisma,如何编写与 prisma 一对多类型匹配的 DTO