欢迎阅读本系列关于使用 NestJS、Prisma 和 PostgreSQL 构建 REST API 的第四篇教程!在本教程中,您将学习如何在 NestJS REST API 中处理关系型数据。

目录
引言
在本系列的第一章中,您创建了一个新的 NestJS 项目并将其与 Prisma、PostgreSQL 和 Swagger 集成。然后,您为博客应用程序的后端构建了一个基本的 REST API。在第二章中,您学习了如何进行输入验证和转换。
在本章中,您将学习如何在数据层和 API 层处理关系型数据。
- 首先,您将向数据库模式添加一个
User
模型,该模型将与Article
记录建立一对多关系(即一个用户可以拥有多篇文章)。 - 接下来,您将为
User
端点实现 API 路由,以对User
记录执行 CRUD(创建、读取、更新和删除)操作。 - 最后,您将学习如何在 API 层中建模 User-Article 关系。
在本教程中,您将使用第二章中构建的 REST API。
开发环境
要跟随本教程,您需要满足以下条件:
- ... 已安装 Node.js。
- ... 已安装 Docker 和 Docker Compose。如果您使用的是 Linux,请确保您的 Docker 版本为 20.10.0 或更高。您可以通过在终端中运行
docker version
来检查您的 Docker 版本。 - ... 可选地,已安装 Prisma VS Code 扩展。Prisma VS Code 扩展为 Prisma 提供了非常好的智能感知和语法高亮功能。
- ... 可选地,可访问 Unix shell(例如 Linux 和 macOS 中的终端/shell)以运行本系列中提供的命令。
如果您没有 Unix shell(例如,您使用的是 Windows 机器),您仍然可以继续学习,但可能需要根据您的机器修改 shell 命令。
克隆仓库
本教程的起点是本系列第二章的结尾。它包含一个使用 NestJS 构建的基本 REST API。
本教程的起点可在 GitHub 仓库的 end-validation
分支中找到。要开始,请克隆仓库并切换到 end-validation
分支
现在,执行以下操作以开始:
- 导航到克隆的目录
- 安装依赖
- 使用 Docker 启动 PostgreSQL 数据库
- 应用数据库迁移
- 启动项目
注意:步骤 4 还会生成 Prisma 客户端并为数据库填充种子数据。
现在,您应该能够访问 http://localhost:3000/api/ 上的 API 文档。
项目结构和文件
您克隆的仓库应具有以下结构:
注意:您可能会注意到此文件夹也带有一个
test
目录。本教程不会涵盖测试。但是,如果您想了解使用 Prisma 测试应用程序的最佳实践,请务必查看本教程系列:《使用 Prisma 进行测试的终极指南》
此仓库中值得注意的文件和目录有:
src
目录包含应用程序的源代码。有三个模块:app
模块位于src
目录的根部,是应用程序的入口点。它负责启动 Web 服务器。prisma
模块包含 Prisma 客户端,它是您与数据库的接口。articles
模块定义了/articles
路由的端点及相关的业务逻辑。
prisma
文件夹包含以下内容:schema.prisma
文件定义了数据库模式。migrations
目录包含数据库迁移历史记录。seed.ts
文件包含一个用于向开发数据库填充虚拟数据的脚本。
docker-compose.yml
文件定义了您的 PostgreSQL 数据库的 Docker 镜像。.env
文件包含您的 PostgreSQL 数据库的连接字符串。
注意:有关这些组件的更多信息,请参阅本教程系列的第一章。
向数据库添加一个 User
模型
目前,您的数据库模式只有一个模型:Article
。一篇文章可以由一个注册用户撰写。因此,您将向数据库模式添加一个 User
模型以反映这种关系。
首先更新您的 Prisma 模式
User
模型有一些您可能期望的字段,例如 id
、email
、password
等。它还与 Article
模型建立了一对多关系。这意味着一个用户可以拥有多篇文章,但一篇文章只能有一个作者。为了简化,author
关系被设置为可选,因此仍然可以在没有作者的情况下创建文章。
现在,要将更改应用到数据库,请运行迁移命令
如果迁移成功运行,您应该看到以下输出:
更新您的种子脚本
种子脚本负责用虚拟数据填充您的数据库。您将更新种子脚本以在数据库中创建一些用户。
打开 prisma/seed.ts
文件并按如下方式更新它:
种子脚本现在创建了两个用户和三篇文章。第一篇文章由第一个用户撰写,第二篇文章由第二个用户撰写,第三篇文章没有作者。
注意:目前,您正在以纯文本形式存储密码。在实际应用程序中绝不应该这样做。您将在下一章中了解更多关于加盐密码和哈希处理的信息。
要执行种子脚本,请运行以下命令:
如果种子脚本成功运行,您应该看到以下输出:
向 ArticleEntity
添加 authorId
字段
运行迁移后,您可能已经注意到一个新的 TypeScript 错误。ArticleEntity
类实现了由 Prisma 生成的 Article
类型。Article
类型有一个新的 authorId
字段,但 ArticleEntity
类没有定义该字段。TypeScript 识别到这种类型不匹配并引发了错误。您将通过向 ArticleEntity
类添加 authorId
字段来修复此错误。
在 ArticleEntity
中添加一个新的 authorId
字段
在像 JavaScript 这样的弱类型语言中,您必须自己识别和修复此类问题。拥有像 TypeScript 这样的强类型语言的一大优势是它可以快速帮助您捕获与类型相关的问题。
为用户实现 CRUD 端点
在本节中,您将在 REST API 中实现 /users
资源。这将允许您对数据库中的用户执行 CRUD 操作。
注意:本节内容将与本系列第一章中“为 Article 模型实现 CRUD 操作”部分的内容相似。该部分更深入地涵盖了该主题,因此您可以阅读它以获得更好的概念理解。
生成新的 users
REST 资源
要为 users
生成新的 REST 资源,请运行以下命令:
您将收到一些 CLI 提示。请相应地回答问题:
您希望为此资源使用什么名称(复数,例如“users”)?
users您使用什么传输层?
REST API您想生成 CRUD 入口点吗?
是
现在,您应该在 src/users
目录中找到一个新的 users
模块,其中包含所有 REST 端点的样板代码。
在 src/users/users.controller.ts
文件中,您将看到不同路由(也称为路由处理程序)的定义。处理每个请求的业务逻辑封装在 src/users/users.service.ts
文件中。
如果您打开 Swagger 生成的 API 页面,您应该会看到类似这样的内容:
将 PrismaClient
添加到 Users
模块
要在 Users
模块中访问 PrismaClient
,您必须将 PrismaModule
作为导入项添加。将以下导入添加到 UsersModule
:
现在,您可以将 PrismaService
注入到 UsersService
中并使用它来访问数据库。为此,请向 users.service.ts
添加一个构造函数,如下所示:
定义 User
实体和 DTO 类
就像 ArticleEntity
一样,您将定义一个 UserEntity
类,它将用于在 API 层中表示 User
实体。在 user.entity.ts
文件中按如下方式定义 UserEntity
类:
@ApiProperty
装饰器用于使属性对 Swagger 可见。请注意,您没有将 @ApiProperty
装饰器添加到 password
字段。这是因为此字段是敏感的,您不希望在 API 中暴露它。
注意:省略
@ApiProperty
装饰器只会将password
属性从 Swagger 文档中隐藏。该属性仍将在响应体中可见。您将在后面的部分中处理此问题。
DTO(数据传输对象)是定义数据如何通过网络发送的对象。您将需要实现 CreateUserDto
和 UpdateUserDto
类,以分别定义在创建和更新用户时将发送到 API 的数据。在 create-user.dto.ts
文件中按如下方式定义 CreateUserDto
类:
@IsString
、@MinLength
和 @IsNotEmpty
是验证装饰器,将用于验证发送到 API 的数据。验证在本系列的第二章中有更详细的介绍。
UpdateUserDto
的定义是从 CreateUserDto
定义中自动推断出来的,因此不需要显式定义。
定义 UsersService
类
UsersService
负责使用 Prisma 客户端修改和获取数据库中的数据,并将其提供给 UsersController
。您将在此类中实现 create()
、findAll()
、findOne()
、update()
和 remove()
方法。
定义 UsersController
类
UsersController
负责处理对 users
端点的请求和响应。它将利用 UsersService
访问数据库,使用 UserEntity
定义响应体,并使用 CreateUserDto
和 UpdateUserDto
定义请求体。
控制器由不同的路由处理程序组成。您将在此类中实现五个路由处理程序,它们对应于五个端点:
create()
-POST /users
findAll()
-GET /users
findOne()
-GET /users/:id
update()
-PATCH /users/:id
remove()
-DELETE /users/:id
按如下方式更新 users.controller.ts
中这些路由处理程序的实现:
更新后的控制器使用 @ApiTags
装饰器将端点分组到 users
标签下。它还使用 @ApiCreatedResponse
和 @ApiOkResponse
装饰器为每个端点定义响应体。
更新后的 Swagger API 页面应如下所示:
请随意测试不同的端点以验证它们的行为是否符合预期。
从响应体中排除 password
字段
尽管 users
API 运行正常,但它存在一个主要安全缺陷。password
字段在不同端点的响应体中返回。
您有两种方法可以解决此问题:
- 在控制器路由处理程序中手动从响应体中移除密码
- 使用拦截器自动从响应体中移除密码
第一种方法容易出错并导致不必要的代码重复。因此,您将使用第二种方法。
使用 ClassSerializerInterceptor
从响应中移除字段
NestJS 中的拦截器允许您介入请求-响应周期,并在路由处理程序执行前后执行额外逻辑。在这种情况下,您将使用它从响应体中移除 password
字段。
NestJS 有一个内置的ClassSerializerInterceptor
,可用于转换对象。您将使用此拦截器从响应对象中移除 password
字段。
首先,通过更新 main.ts
全局启用 ClassSerializerInterceptor
注意: 也可以将拦截器绑定到方法或控制器,而不是全局绑定。您可以在 NestJS 文档中了解更多信息。
ClassSerializerInterceptor
使用 class-transformer
包来定义如何转换对象。使用 @Exclude()
装饰器在 UserEntity
类中排除 password
字段
如果您再次尝试使用 GET /users/:id
端点,您会注意到 password
字段仍然被暴露 🤔。这是因为,目前控制器中的路由处理程序返回的是由 Prisma 客户端生成的 User
类型。ClassSerializerInterceptor
仅适用于使用 @Exclude()
装饰器装饰的类。在这种情况下,它是 UserEntity
类。因此,您需要更新路由处理程序以返回 UserEntity
类型。
首先,您需要创建一个构造函数,它将实例化一个 UserEntity
对象。
构造函数接受一个对象,并使用 Object.assign()
方法将 partial
对象的属性复制到 UserEntity
实例。partial
的类型是 Partial<UserEntity>
。这意味着 partial
对象可以包含 UserEntity
类中定义的任何属性子集。
接下来,更新 UsersController
路由处理程序以返回 UserEntity
而不是 Prisma.User
对象
现在,密码应该从响应对象中省略。
返回作者和文章
在第一章中,您实现了 GET /articles/:id
端点以检索单篇文章。目前,此端点不返回文章的 author
,只返回 authorId
。为了获取 author
,您必须向 GET /users/:id
端点发出额外的请求。如果您需要文章及其作者,这并不理想,因为您需要发出两次 API 请求。您可以通过将 author
与 Article
对象一起返回来改进这一点。
数据访问逻辑在 ArticlesService
中实现。更新 findOne()
方法以返回 author
以及 Article
对象
如果您测试 GET /articles/:id
端点,您会注意到文章的作者(如果存在)包含在响应对象中。但是,有一个问题。password
字段再次暴露了 🤦。
这个问题的原因与上次非常相似。目前,ArticlesController
返回 Prisma 生成类型的实例,而 ClassSerializerInterceptor
适用于 UserEntity
类。要解决此问题,您将更新 ArticleEntity
类的实现,并确保它使用 UserEntity
的实例初始化 author
属性。
您再次使用 Object.assign()
方法将 data
对象的属性复制到 ArticleEntity
实例。author
属性(如果存在)被初始化为 UserEntity
的实例。
现在,更新 ArticlesController
以返回 ArticleEntity
对象的实例
现在,GET /articles/:id
返回的 author
对象不包含 password
字段
总结和最终说明
在本章中,您学习了如何使用 Prisma 在 NestJS 应用程序中建模关系型数据。您还了解了 ClassSerializerInterceptor
以及如何使用实体类来控制返回给客户端的数据。
您可以在 GitHub 仓库的 end-relational-data
分支中找到本教程的完整代码。如果您发现问题,请随时在仓库中提出问题或提交 PR。您也可以直接在 Twitter 上联系我。
不要错过下一篇文章!
订阅 Prisma 邮件通讯