2023年3月23日

使用 NestJS 和 Prisma 構建 REST API:處理關係型資料

8 分鐘閱讀

歡迎閱讀本系列關於使用 NestJS、Prisma 和 PostgreSQL 構建 REST API 的第四篇教程!在本教程中,您將學習如何在 NestJS REST API 中處理關係型資料。

Building a REST API with NestJS and Prisma: Handling Relational Data

目錄

引言

在本系列的第一章中,您建立了一個新的 NestJS 專案並將其與 Prisma、PostgreSQL 和 Swagger 整合。然後,您為部落格應用程式的後端構建了一個基本的 REST API。在第二章中,您學習瞭如何進行輸入驗證和轉換。

在本章中,您將學習如何在資料層和 API 層處理關係型資料。

  1. 首先,您將向資料庫模式新增一個 User 模型,該模型將與 Article 記錄建立一對多關係(即一個使用者可以擁有多篇文章)。
  2. 接下來,您將為 User 端點實現 API 路由,以對 User 記錄執行 CRUD(建立、讀取、更新和刪除)操作。
  3. 最後,您將學習如何在 API 層中建模 User-Article 關係。

在本教程中,您將使用第二章中構建的 REST API。

開發環境

要跟隨本教程,您需要滿足以下條件:

  • ... 已安裝 Node.js
  • ... 已安裝 DockerDocker 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 分支

現在,執行以下操作以開始:

  1. 導航到克隆的目錄
  1. 安裝依賴
  1. 使用 Docker 啟動 PostgreSQL 資料庫
  1. 應用資料庫遷移
  1. 啟動專案

注意:步驟 4 還會生成 Prisma 客戶端併為資料庫填充種子資料。

現在,您應該能夠訪問 https://: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 模型有一些您可能期望的欄位,例如 idemailpassword 等。它還與 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 提示。請相應地回答問題:

  1. 您希望為此資源使用什麼名稱(複數,例如“users”)? users
  2. 您使用什麼傳輸層? REST API
  3. 您想生成 CRUD 入口點嗎?

現在,您應該在 src/users 目錄中找到一個新的 users 模組,其中包含所有 REST 端點的樣板程式碼。

src/users/users.controller.ts 檔案中,您將看到不同路由(也稱為路由處理程式)的定義。處理每個請求的業務邏輯封裝在 src/users/users.service.ts 檔案中。

如果您開啟 Swagger 生成的 API 頁面,您應該會看到類似這樣的內容:

Auto-generated "users" endpoints

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(資料傳輸物件)是定義資料如何透過網路傳送的物件。您將需要實現 CreateUserDtoUpdateUserDto 類,以分別定義在建立和更新使用者時將傳送到 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 定義響應體,並使用 CreateUserDtoUpdateUserDto 定義請求體。

控制器由不同的路由處理程式組成。您將在此類中實現五個路由處理程式,它們對應於五個端點:

  • 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 頁面應如下所示:

Updated swagger page

請隨意測試不同的端點以驗證它們的行為是否符合預期。

從響應體中排除 password 欄位

儘管 users API 執行正常,但它存在一個主要安全缺陷。password 欄位在不同端點的響應體中返回。

GET /users/:id reveals password

您有兩種方法可以解決此問題:

  1. 在控制器路由處理程式中手動從響應體中移除密碼
  2. 使用攔截器自動從響應體中移除密碼

第一種方法容易出錯並導致不必要的程式碼重複。因此,您將使用第二種方法。

使用 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 /users/:id does not reveal password

返回作者和文章

第一章中,您實現了 GET /articles/:id 端點以檢索單篇文章。目前,此端點不返回文章的 author,只返回 authorId。為了獲取 author,您必須向 GET /users/:id 端點發出額外的請求。如果您需要文章及其作者,這並不理想,因為您需要發出兩次 API 請求。您可以透過將 authorArticle 物件一起返回來改進這一點。

資料訪問邏輯在 ArticlesService 中實現。更新 findOne() 方法以返回 author 以及 Article 物件

如果您測試 GET /articles/:id 端點,您會注意到文章的作者(如果存在)包含在響應物件中。但是,有一個問題。password 欄位再次暴露了 🤦。

GET /articles/:id reveals password

這個問題的原因與上次非常相似。目前,ArticlesController 返回 Prisma 生成型別的例項,而 ClassSerializerInterceptor 適用於 UserEntity 類。要解決此問題,您將更新 ArticleEntity 類的實現,並確保它使用 UserEntity 的例項初始化 author 屬性。

您再次使用 Object.assign() 方法將 data 物件的屬性複製到 ArticleEntity 例項。author 屬性(如果存在)被初始化為 UserEntity 的例項。

現在,更新 ArticlesController 以返回 ArticleEntity 物件的例項

現在,GET /articles/:id 返回的 author 物件不包含 password 欄位

GET /articles/:id does not reveal password

總結和最終說明

在本章中,您學習瞭如何使用 Prisma 在 NestJS 應用程式中建模關係型資料。您還了解了 ClassSerializerInterceptor 以及如何使用實體類來控制返回給客戶端的資料。

您可以在 GitHub 倉庫的 end-relational-data 分支中找到本教程的完整程式碼。如果您發現問題,請隨時在倉庫中提出問題或提交 PR。您也可以直接在 Twitter 上聯絡我。

不要錯過下一篇文章!

訂閱 Prisma 郵件通訊

© . This site is unofficial and not affiliated with Prisma Data, Inc.