欢迎来到本系列教程的第四部分,关于使用 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 添加了一些非常好的 IntelliSense 和语法高亮。
- ... (可选)可以访问 Unix shell(例如 Linux 和 macOS 中的终端/shell)来运行本系列中提供的命令。
如果您没有 Unix shell(例如,您使用的是 Windows 机器),您仍然可以继续学习,但可能需要为您的机器修改 shell 命令。
克隆存储库
本教程的起点是本系列教程第二章的结尾。它包含一个使用 NestJS 构建的基本 REST API。
本教程的起点可在 end-validation
分支的 GitHub 存储库中找到。要开始使用,请克隆存储库并检出 end-validation
分支
现在,执行以下操作以开始
- 导航到克隆的目录
- 安装依赖项
- 使用 Docker 启动 PostgreSQL 数据库
- 应用数据库迁移
- 启动项目
注意:步骤 4 也会生成 Prisma Client 并播种数据库。
现在,您应该可以通过 https://127.0.0.1:3000/api/
访问 API 文档。
项目结构和文件
您克隆的存储库应具有以下结构
注意:您可能会注意到此文件夹也带有一个
test
目录。本教程不涉及测试。但是,如果您想了解使用 Prisma 测试应用程序的最佳实践,请务必查看本教程系列:《Prisma 测试终极指南》
此存储库中值得注意的文件和目录是
src
目录包含应用程序的源代码。有三个模块app
模块位于src
目录的根目录,是应用程序的入口点。它负责启动 Web 服务器。prisma
模块包含 Prisma Client,即您与数据库的接口。articles
模块定义了/articles
路由的端点和随附的业务逻辑。
prisma
文件夹包含以下内容schema.prisma
文件定义了数据库模式。migrations
目录包含数据库迁移历史记录。seed.ts
文件包含一个脚本,用于使用虚拟数据播种您的开发数据库。
docker-compose.yml
文件定义了 PostgreSQL 数据库的 Docker 镜像。.env
文件包含 PostgreSQL 数据库的数据库连接字符串。
注意:有关这些组件的更多信息,请阅读本教程系列的第一章。
向数据库添加 User
模型
目前,您的数据库模式只有一个模型:Article
。文章可以由注册用户撰写。因此,您将在数据库模式中添加一个 User
模型以反映这种关系。
首先更新您的 Prisma 模式
User
模型有一些您可能期望的字段,例如 id
、email
、password
等。它还与 Article
模型具有一对多关系。这意味着一个用户可以拥有多篇文章,但一篇文章只能有一个作者。为了简单起见,作者关系是可选的,因此仍然可以在没有作者的情况下创建文章。
现在,要将更改应用到您的数据库,请运行迁移命令
如果迁移成功运行,您应该看到以下输出
更新你的种子脚本
种子脚本负责使用虚拟数据填充您的数据库。您将更新种子脚本以在数据库中创建一些用户。
打开 prisma/seed.ts
文件并按如下方式更新它
种子脚本现在创建了两个用户和三篇文章。第一篇文章由第一个用户撰写,第二篇文章由第二个用户撰写,第三篇文章由无人撰写。
注意:目前,您以纯文本形式存储密码。在实际应用程序中,您绝不应该这样做。您将在下一章中了解有关密码加盐和哈希处理的更多信息。
要执行种子脚本,请运行以下命令
如果种子脚本成功运行,您应该看到以下输出
向 ArticleEntity
添加 authorId
字段
运行迁移后,您可能已经注意到一个新的 TypeScript 错误。ArticleEntity
类 implements
了 Prisma 生成的 Article
类型。Article
类型有一个新的 authorId
字段,但 ArticleEntity
类没有定义该字段。TypeScript 识别出类型不匹配并引发错误。您将通过向 ArticleEntity
类添加 authorId
字段来修复此错误。
在 ArticleEntity
内部添加一个新的 authorId
字段
在像 JavaScript 这样的弱类型语言中,您必须自己识别和修复此类问题。拥有像 TypeScript 这样的强类型语言的一大优势是它可以快速帮助您捕获与类型相关的问题。
为 Users 实现 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 页面,您应该看到类似这样的内容
向 Users 模块添加 PrismaClient
要在 Users
模块内部访问 PrismaClient
,您必须添加 PrismaModule
作为导入。将以下 imports
添加到 UsersModule
您现在可以将 PrismaService
注入 UsersService
内部,并使用它来访问数据库。为此,请像这样向 users.service.ts
添加一个构造函数
定义 User 实体和 DTO 类
就像 ArticleEntity
一样,您将定义一个 UserEntity
类,该类将用于表示 API 层中的 User
实体。在 user.entity.ts
文件中定义 UserEntity
类,如下所示
@ApiProperty
装饰器用于使属性对 Swagger 可见。请注意,您没有向 password
字段添加 @ApiProperty
装饰器。这是因为此字段很敏感,您不想在 API 中公开它。
注意:省略
@ApiProperty
装饰器只会从 Swagger 文档中隐藏password
属性。该属性仍然在响应体中可见。您将在后面的部分中处理此问题。
DTO(数据传输对象)是一个定义数据如何通过网络发送的对象。您需要实现 CreateUserDto
和 UpdateUserDto
类,以分别定义在创建和更新用户时将发送到 API 的数据。在 create-user.dto.ts
文件中定义 CreateUserDto
类,如下所示
@IsString
、@MinLength
和 @IsNotEmpty
是验证装饰器,将用于验证发送到 API 的数据。本系列教程的第二章更详细地介绍了验证。
UpdateUserDto
的定义是从 CreateUserDto
定义自动推断出来的,因此不需要显式定义。
定义 UsersService 类
UsersService
负责使用 Prisma Client 修改和从数据库中获取数据,并将其提供给 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 Client 生成的 User
类型。ClassSerializerInterceptor
仅适用于使用 @Exclude()
装饰器装饰的类。在本例中,它是 UserEntity
类。因此,您需要更新路由处理程序以返回 UserEntity
类型。
首先,您需要创建一个构造函数来实例化 UserEntity
对象。
构造函数接受一个对象,并使用 Object.assign()
方法将属性从 partial
对象复制到 UserEntity
实例。partial
的类型是 Partial<UserEntity>
。这意味着 partial
对象可以包含 UserEntity
类中定义的属性的任何子集。
接下来,更新 UsersController
路由处理程序以返回 UserEntity
而不是 Prisma.User
对象
现在,密码应该从响应对象中省略。
返回文章的同时返回作者
在第一章中,您实现了用于检索单个文章的 GET /articles/:id
端点。目前,此端点不返回文章的 author
,仅返回 authorId
。为了获取作者,您必须向 GET /users/:id
端点发出额外的请求。如果您同时需要文章及其作者,这并不理想,因为您需要发出两个 API 请求。您可以通过返回 Article
对象的同时返回 author
来改进这一点。
数据访问逻辑在 ArticlesService
内部实现。更新 findOne()
方法以返回 Article
对象的同时返回 author
如果您测试 GET /articles/:id
端点,您会注意到文章的作者(如果存在)包含在响应对象中。但是,有一个问题。password
字段再次公开 🤦。
此问题的原因与上次非常相似。目前,ArticlesController
返回 Prisma 生成类型的实例,而 ClassSerializerInterceptor
与 UserEntity
类一起使用。要解决此问题,您将更新 ArticleEntity
类的实现,并确保它使用 UserEntity
的实例初始化 author
属性。
再一次,您使用 Object.assign()
方法将属性从 data
对象复制到 ArticleEntity
实例。author
属性(如果存在)被初始化为 UserEntity
的实例。
现在更新 ArticlesController
以返回 ArticleEntity
对象的实例
现在,GET /articles/:id
返回不带 password
字段的作者对象
总结和最终说明
在本章中,您学习了如何使用 Prisma 在 NestJS 应用程序中建模关系数据。您还了解了 ClassSerializerInterceptor
以及如何使用实体类来控制返回给客户端的数据。
您可以在 GitHub 存储库的 end-relational-data
分支中找到本教程的完成代码。如果您发现问题,请随时在存储库中提出 issue 或提交 PR。您也可以直接在 Twitter 上与我联系。
不要错过下一篇文章!
注册 Prisma 新闻通讯