歡迎來到本系列教學的第四篇,關於使用 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
模型具有一對多關係。這表示一個使用者可以擁有多篇文章,但一篇文章只能有一個作者。為了簡單起見,author
關係設為選用,因此仍然可以建立沒有作者的文章。
現在,若要將變更套用至您的資料庫,請執行遷移命令
如果遷移成功執行,您應該會看到以下輸出
更新您的種子指令碼
種子指令碼負責使用虛擬資料填入您的資料庫。您將更新種子指令碼,以便在您的資料庫中建立一些使用者。
開啟 prisma/seed.ts
檔案並按如下所示更新它
種子指令碼現在會建立兩個使用者和三篇文章。第一篇文章由第一個使用者撰寫,第二篇文章由第二個使用者撰寫,第三篇文章則由沒有人撰寫。
注意:目前,您是以純文字形式儲存密碼。您絕不應該在真實應用程式中執行此操作。您將在下一章中學習更多關於密碼加鹽和雜湊的知識。
若要執行種子指令碼,請執行以下命令
如果種子指令碼成功執行,您應該會看到以下輸出
將 authorId
欄位新增至 ArticleEntity
執行遷移後,您可能已經注意到一個新的 TypeScript 錯誤。ArticleEntity
類別 implements
由 Prisma 產生的 Article
類型。Article
類型有一個新的 authorId
欄位,但 ArticleEntity
類別未定義該欄位。TypeScript 識別到類型中的這種不符,並引發錯誤。您將透過將 authorId
欄位新增至 ArticleEntity
類別來修正此錯誤。
在 ArticleEntity
內部新增一個新的 authorId
欄位
在像 JavaScript 這樣的弱類型語言中,您必須自己識別和修正此類問題。擁有像 TypeScript 這樣的強類型語言的優勢之一是它可以快速協助您捕捉與類型相關的問題。
實作使用者的 CRUD 端點
在本節中,您將在您的 REST API 中實作 /users
資源。這將允許您對資料庫中的使用者執行 CRUD 操作。
注意:本節的內容將與本系列教學第一章中的 實作 Article 模型的 CRUD 操作 節的內容相似。該節更深入地介紹了該主題,因此您可以閱讀它以獲得更好的概念理解。
產生新的 users
REST 資源
若要為 users
產生新的 REST 資源,請執行以下命令
系統會為您提供一些 CLI 提示。相應地回答問題
您想要為此資源使用的名稱(複數,例如 "users")?
users您使用哪種傳輸層?
REST API您想要產生 CRUD 進入點嗎?
Yes
您現在應該會在 src/users
目錄中找到一個新的 users
模組,其中包含您的 REST 端點的所有樣板。
在 src/users/users.controller.ts
檔案中,您將看到不同路由(也稱為路由處理常式)的定義。處理每個請求的業務邏輯封裝在 src/users/users.service.ts
檔案中。
如果您開啟 Swagger 產生的 API 頁面,您應該會看到類似這樣的內容
將 PrismaClient
新增至 Users
模組
若要在 Users
模組內存取 PrismaClient
,您必須將 PrismaModule
新增為匯入項。將以下 imports
新增至 UsersModule
您現在可以在 UsersService
內部注入 PrismaService
,並使用它來存取資料庫。若要執行此操作,請將建構函式新增至 users.service.ts
,如下所示
定義 User
實體和 DTO 類別
就像 ArticleEntity
一樣,您將定義一個 UserEntity
類別,它將用於表示 API 層中的 User
實體。在 user.entity.ts
檔案中定義 UserEntity
類別,如下所示
@ApiProperty
裝飾器用於使屬性對 Swagger 可見。請注意,您未將 @ApiProperty
裝飾器新增至 password
欄位。這是因為此欄位很敏感,您不希望在您的 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
。為了擷取 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
,以及如何使用實體類別來控制回傳給客戶端的資料。
你可以在 end-relational-data
分支的 GitHub 儲存庫中找到此教學的完成程式碼。如果你注意到任何問題,請隨時在儲存庫中提出 issue 或提交 PR。你也可以在 Twitter 上直接聯繫我。
別錯過下一篇文章!
訂閱 Prisma 電子報