为 NestJs REST 创建 DTO、BO 和 DAO API
create DTOs, BOs and DAOs for NestJs REST API
我想开始使用 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
}
我安装了 NestJs 提供的 DTOs and want to validate the request first. I use the 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 切换到整数。 "find by 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
函数。
否则我可以创建 RequestDAOs
共享一个..
export interface IdDAO {
id: string;
}
和 POST DAO
export interface CreateDAO {
username: string;
password: string;
}
有了它,我可以在我的存储库中创建一个数据库用户,并使用 ResponseDAOs 映射数据库响应,但这始终是没有密码信息的整个数据库用户。好像又产生了很大的开销。
我想知道我使用 3 个请求和 3 个响应接口的方法是否太多了并且可以简化。但我想保留一个灵活的层,因为我认为这些层应该是高度独立的……另一方面,那里会有大量的模型。
提前致谢!
我通过使用 class-transformer
库(NestJs 推荐)来代表用户(内部和外部)来处理公开用户和内部用户之间的差异来处理此问题没有定义两个 classes.
的用户
这是一个使用您的用户模型的示例:
定义用户Class
由于这个用户 class 被保存到数据库中,我通常为每个数据库对象期望拥有的所有字段创建一个基础 class。比方说:
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());
}
}
接下来,我们的用户将花费这个基本的class:
@Exclude()
export class User extends BaseDBObject {
@Expose()
username: string;
password: string;
constructor(partial: Partial<User> = {}) {
super();
Object.assign(this, partial);
}
}
当我们在服务器外部公开 class 时,我在这里使用 class-transformer
库中的一些装饰器来更改此内部用户(所有数据库字段都完好无损)。
@Expose
- 如果 class 默认为排除 ,则将公开该属性
@Exclude
- 将排除 属性 如果 class- 默认是公开
@Transform
- 在 'exporting' 时更改属性名称
这意味着在 运行 来自 class-transformer
的 classToPlain
函数之后,将应用我们在给定 class 上定义的所有规则。
控制器
NestJs
有一个你添加的装饰器,以确保 class 你从控制器端点 return 将使用 classToPlain
函数来转换对象,return 忽略所有私有字段和转换的结果对象(比如将 _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());
}
}
}
因为在内部我们总是使用用户 class,我将数据 return 从数据库转换为用户 class 实例。
我正在使用 @nestjs/mongoose
,但基本上在从数据库中检索用户后,mongoose
和 TypeORM
.
的一切都是一样的
注意事项
使用 @nestjs/mongoose
,我无法避免创建 IUser
接口以传递给 mongo Model
class 因为它需要一些扩展mongodb Document
export interface IUser extends mongoose.Document {
username: string;
password: string;
}
当获取用户时,API 将 return 转换为 JSON:
{
"id": "5e1452f93794e82db588898e",
"username": "username"
}
Here's the code for this example in a GitHub repository.
更新
如果您想查看使用 typegoose
消除接口的示例(基于 this blog post), take a look here for a model, and here for the base model
我想开始使用 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
}
我安装了 NestJs 提供的 DTOs and want to validate the request first. I use the 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 切换到整数。 "find by 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
函数。
否则我可以创建 RequestDAOs
共享一个..
export interface IdDAO {
id: string;
}
和 POST DAO
export interface CreateDAO {
username: string;
password: string;
}
有了它,我可以在我的存储库中创建一个数据库用户,并使用 ResponseDAOs 映射数据库响应,但这始终是没有密码信息的整个数据库用户。好像又产生了很大的开销。
我想知道我使用 3 个请求和 3 个响应接口的方法是否太多了并且可以简化。但我想保留一个灵活的层,因为我认为这些层应该是高度独立的……另一方面,那里会有大量的模型。
提前致谢!
我通过使用 class-transformer
库(NestJs 推荐)来代表用户(内部和外部)来处理公开用户和内部用户之间的差异来处理此问题没有定义两个 classes.
这是一个使用您的用户模型的示例:
定义用户Class
由于这个用户 class 被保存到数据库中,我通常为每个数据库对象期望拥有的所有字段创建一个基础 class。比方说:
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());
}
}
接下来,我们的用户将花费这个基本的class:
@Exclude()
export class User extends BaseDBObject {
@Expose()
username: string;
password: string;
constructor(partial: Partial<User> = {}) {
super();
Object.assign(this, partial);
}
}
当我们在服务器外部公开 class 时,我在这里使用 class-transformer
库中的一些装饰器来更改此内部用户(所有数据库字段都完好无损)。
@Expose
- 如果 class 默认为排除 ,则将公开该属性
@Exclude
- 将排除 属性 如果 class- 默认是公开@Transform
- 在 'exporting' 时更改属性名称
这意味着在 运行 来自 class-transformer
的 classToPlain
函数之后,将应用我们在给定 class 上定义的所有规则。
控制器
NestJs
有一个你添加的装饰器,以确保 class 你从控制器端点 return 将使用 classToPlain
函数来转换对象,return 忽略所有私有字段和转换的结果对象(比如将 _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());
}
}
}
因为在内部我们总是使用用户 class,我将数据 return 从数据库转换为用户 class 实例。
我正在使用 @nestjs/mongoose
,但基本上在从数据库中检索用户后,mongoose
和 TypeORM
.
注意事项
使用 @nestjs/mongoose
,我无法避免创建 IUser
接口以传递给 mongo Model
class 因为它需要一些扩展mongodb Document
export interface IUser extends mongoose.Document {
username: string;
password: string;
}
当获取用户时,API 将 return 转换为 JSON:
{
"id": "5e1452f93794e82db588898e",
"username": "username"
}
Here's the code for this example in a GitHub repository.
更新
如果您想查看使用 typegoose
消除接口的示例(基于 this blog post), take a look here for a model, and here for the base model