本文是一系列關於使用TypeScript、PostgreSQL和Prisma構建後端直播和文章的一部分。在本文中,我們將探討如何構建REST API、驗證輸入以及測試API。

簡介
本系列的目標是透過解決一個具體問題:線上課程的評分系統,探索並演示現代後端開發中的不同模式、問題和架構。這是一個很好的例子,因為它具有多樣化的關係型別,並且足夠複雜,足以代表一個真實世界的用例。
直播錄影可在上方觀看,內容與本文相同。
本系列將涵蓋的內容
本系列將側重於資料庫在後端開發各個方面的作用,包括
今天您將學到什麼
在第一篇文章中,您為問題領域設計了資料模型,並編寫了一個使用Prisma Client將資料儲存到資料庫的種子指令碼。
在本系列的第二篇文章中,您將在第一篇文章中的資料模型和Prisma schema之上構建一個REST API。您將使用Hapi來構建REST API。透過REST API,您將能夠透過HTTP請求執行資料庫操作。
作為REST API的一部分,您將開發以下方面:
- REST API:實現一個帶有資源端點的HTTP伺服器,用於處理不同模型的CRUD操作。您將把Prisma與Hapi整合,以便API端點處理程式可以訪問Prisma Client。
- 驗證:新增有效負載驗證規則,以確保使用者輸入與Prisma schema的預期型別匹配。
- 測試:使用Jest和Hapi的
server.inject為REST端點編寫測試,模擬HTTP請求,驗證REST端點的驗證和持久化邏輯。
在本文結束時,您將擁有一個帶有CRUD(建立、讀取、更新和刪除)操作端點和測試的REST API。REST資源將HTTP請求對映到Prisma schema中的模型,例如,GET /users端點將處理與User模型相關的操作。
本系列的下一部分將詳細介紹列表中的其他方面。
注意:在整個指南中,您會發現各種檢查點,它們能幫助您驗證是否正確執行了步驟。
先決條件
假定知識
本系列假定您具備TypeScript、Node.js和關係資料庫的基礎知識。如果您有JavaScript經驗但尚未嘗試TypeScript,您仍然可以繼續學習。本系列將使用PostgreSQL,但大多數概念也適用於其他關係資料庫,例如MySQL。此外,熟悉REST概念也很有用。除此之外,不需要Prisma的先驗知識,因為這將在系列中涵蓋。
開發環境
您應該安裝以下軟體:
如果您正在使用Visual Studio Code,建議安裝Prisma擴充套件,用於語法高亮、格式化和其他輔助功能。
注意:如果您不想使用Docker,您可以設定一個本地PostgreSQL資料庫或在Heroku上託管的PostgreSQL資料庫。
克隆程式碼庫
本系列的原始碼可在GitHub上找到。
開始之前,請克隆程式碼庫並安裝依賴項:
注意:透過檢出
part-2分支,您將能夠從相同的起點跟隨本文。
啟動PostgreSQL
要啟動PostgreSQL,請在real-world-grading-app資料夾中執行以下命令:
注意:Docker將使用
docker-compose.yml檔案來啟動PostgreSQL容器。
構建REST API
在深入實現之前,我們將回顧一些與REST API相關的基本概念:
- API:應用程式程式設計介面。一組允許程式相互通訊的規則。通常,開發者在伺服器上建立API,並允許客戶端與之通訊。
- REST:開發者遵循的一組約定,用於透過HTTP請求公開與狀態相關的操作(在本例中是儲存在資料庫中的狀態)。例如,請檢視GitHub REST API。
- 端點:REST API的入口點,具有以下屬性(非詳盡):
- 路徑,例如
/users/,用於訪問使用者端點。路徑決定了用於訪問端點的URL,例如www.myapi.com/users/。 - HTTP方法,例如
GET、POST和DELETE。HTTP方法將決定端點公開的操作型別,例如,GET /users端點將允許獲取使用者,POST /users端點將允許建立使用者。 - 處理程式:將處理端點請求的程式碼(在本例中為TypeScript)。
- 路徑,例如
- HTTP狀態碼:響應的HTTP狀態碼將告知API消費者操作是否成功以及是否發生任何錯誤。請檢視此列表以瞭解不同的HTTP狀態碼,例如,資源成功建立時為
201,消費者輸入驗證失敗時為400。
注意:REST方法的一個關鍵目標是使用HTTP作為應用協議,透過遵循約定來避免重複造輪子。
API端點
API將包含以下端點(HTTP方法後跟路徑):
UserPOST/users建立使用者(可選地與課程關聯)UserGET/users/{userId}獲取使用者UserPUT/users/{userId}更新使用者UserDELETE/users/{userId}刪除使用者UserGET/users獲取使用者列表CourseEnrollmentGET/users/{userId}/courses獲取使用者的課程註冊資訊CourseEnrollmentPOST/users/{userId}/courses將使用者註冊到課程(作為學生或教師)CourseEnrollmentDELETE/users/{userId}/courses/{courseId}刪除使用者在課程中的註冊CoursePOST/courses建立課程CourseGET/courses獲取課程列表CourseGET/courses/{courseId}獲取課程CoursePUT/courses/{courseId}更新課程CourseDELETE/courses/{courseId}刪除課程TestPOST/courses/{courseId}/tests為課程建立測試TestGET/courses/tests/{testId}獲取測試TestPUT/courses/tests/{testId}更新測試TestDELETE/courses/tests/{testId}刪除測試Test ResultGET/users/{userId}/test-results獲取使用者的測試結果Test ResultPOST/courses/tests/{testId}/test-results為與使用者關聯的測試建立測試結果Test ResultGET/courses/tests/{testId}/test-results獲取測試的多個測試結果Test ResultPUT/courses/tests/test-results/{testResultId}更新測試結果(與使用者和測試關聯)Test ResultDELETE/courses/tests/test-results/{testResultId}刪除測試結果注意:包含在
{}中引數的路徑,例如{userId},表示URL中插入的變數,例如在www.myapi.com/users/13中,userId是13。
以上端點已根據它們關聯的主模型/資源進行分組。這種分類有助於將程式碼組織成獨立的模組,以提高可維護性。
在本文中,您將實現上述端點的一個子集(前四個),以說明不同CRUD操作的不同模式。完整的API將在GitHub程式碼庫中提供。這些端點應提供大多數操作的介面。雖然某些資源沒有用於刪除資源的DELETE端點,但它們可以在以後新增。
注意:在本文中,端點(endpoint)和路由(route)這兩個詞將交替使用。儘管它們指代同一事物,但在REST語境中稱為端點,而在HTTP伺服器語境中稱為路由。
Hapi
API將使用Hapi構建——這是一個用於構建HTTP伺服器的Node.js框架,它開箱即用地支援驗證和測試。
Hapi由一個名為@hapi/hapi的核心模組(即HTTP伺服器)和擴充套件核心功能的模組組成。在這個後端中,您還將使用以下模組:
@hapi/joi用於宣告式輸入驗證@hapi/boom用於生成HTTP友好的錯誤物件
要使Hapi與TypeScript一起工作,您需要新增Hapi和Joi的型別定義。這是因為Hapi是用JavaScript編寫的。透過新增型別,您將擁有豐富的自動補全功能,並允許TypeScript編譯器確保程式碼的型別安全。
安裝以下包:
建立伺服器
您首先需要做的是建立一個Hapi伺服器,它將繫結到一個介面和埠。
將以下Hapi伺服器程式碼新增到src/server.ts
首先,您匯入Hapi。然後初始化一個新的Hapi.server()(型別為@types/hapi__hapi包中定義的Hapi.Server),其中包含要監聽的埠號和主機資訊。之後,您啟動伺服器並記錄它正在執行。
要在開發期間在本地執行伺服器,請執行npm dev指令碼,該指令碼將使用ts-node-dev自動轉譯TypeScript程式碼並在您進行更改時重新啟動伺服器:npm run dev
檢查點:如果您在瀏覽器中開啟https://:3000,您應該會看到以下內容:{"statusCode":404,"error":"Not Found","message":"Not Found"}
恭喜,您已成功建立了一個伺服器。然而,該伺服器尚未定義任何路由。在下一步中,您將定義第一個路由。
定義路由
要新增路由,您將在上一步中例項化的Hapi server上使用route()方法。在定義與業務邏輯相關的路由之前,新增一個返回200 HTTP狀態碼的/status端點是良好的實踐。這對於確保伺服器正常執行非常有用。
為此,請更新server.ts中的start函式,在頂部新增以下內容:
這裡您定義了HTTP方法、路徑以及一個返回物件{ up: true }的處理程式,最後將HTTP狀態碼設定為200。
檢查點:如果您在瀏覽器中開啟https://:3000,您應該會看到以下內容:{"up":true}
將路由移至外掛
在上一步中,您定義了一個狀態端點。由於API將暴露許多不同的端點,將所有端點都定義在start函式中將難以維護。
Hapi引入了外掛的概念,作為將後端分解為獨立業務邏輯模組的一種方式。外掛是保持程式碼模組化的精簡方法。在這一步中,您將把上一步中定義的路由移到外掛中。
這需要兩個步驟:
- 在新檔案中定義外掛。
- 在呼叫
server.start()之前註冊外掛。
定義外掛
首先,在src/中建立一個名為plugins的新資料夾。
在src/plugins/資料夾中建立一個名為status.ts的新檔案。
並向檔案中新增以下內容:
Hapi外掛是一個包含name屬性和register函式的物件,您通常在此處封裝外掛的邏輯。name屬性是外掛名稱字串,用作唯一鍵。
每個外掛都可以透過標準伺服器介面操作伺服器。在上面的app/status外掛中,server用於在register函式中定義狀態路由。
註冊外掛
要註冊外掛,請返回到server.ts並按如下方式匯入狀態外掛:
在start函式中,用以下server.register()呼叫替換上一步中的route()呼叫:
檢查點:如果您在瀏覽器中開啟https://:3000,您應該會看到以下內容:{"up":true}
恭喜,您已成功建立了一個Hapi外掛,它封裝了狀態端點的邏輯。
在下一步中,您將定義一個測試來測試狀態端點。
為狀態端點定義測試
為了測試狀態端點,您將使用Jest作為測試執行器,並結合Hapi的server.inject測試輔助函式來模擬對伺服器的HTTP請求。這將使您能夠驗證是否正確實現了該端點。
將server.ts拆分為兩個檔案
要使用server.inject方法,您需要在測試中訪問已註冊外掛但尚未啟動伺服器的server物件,以避免在測試執行時伺服器監聽請求。為此,請修改server.ts如下:
您剛剛將start函式拆分並替換為兩個函式:
createServer():註冊外掛並初始化伺服器startServer():啟動伺服器
注意:Hapi的
server.initialize()會初始化伺服器(啟動快取,完成外掛註冊),但不會開始監聽連線埠。
現在您可以匯入server.ts並在測試中使用createServer()來初始化伺服器,然後呼叫server.inject()來模擬HTTP請求。
接下來,您將為應用程式建立一個新的入口點,它將同時呼叫createServer()和startServer()。
建立一個新的src/index.ts檔案,並向其中新增以下內容:
最後,更新package.json中的dev指令碼,使其啟動src/index.ts而不是src/server.ts。
建立測試
要建立測試,請在專案根目錄下建立一個名為tests的資料夾,並建立一個名為status.test.ts的檔案,然後向檔案中新增以下內容:
在上面的測試中,beforeAll和afterAll用作建立和停止伺服器的setup和teardown函式。
然後,呼叫server.inject來模擬對根端點/的GET HTTP請求。測試接著斷言HTTP狀態碼和有效負載,以確保它們與處理程式匹配。
檢查點:使用npm test命令執行測試,您應該看到以下輸出:
恭喜,您已建立了一個帶有路由的外掛並測試了該路由。
在下一步中,您將定義一個Prisma外掛,以便您可以在整個應用程式中訪問Prisma Client例項。
定義Prisma外掛
與建立狀態外掛類似,為Prisma外掛建立一個新檔案src/plugins/prisma.ts。
Prisma外掛的目標是例項化Prisma Client,透過server.app物件使其可供應用程式的其他部分使用,並在伺服器停止時斷開與資料庫的連線。server.app提供了一個安全的儲存位置,用於儲存伺服器特定的執行時應用程式資料,而不會與框架內部發生潛在衝突。只要伺服器可訪問,就可以訪問這些資料。
將以下內容新增到src/plugins/prisma.ts檔案中:
在這裡,我們定義一個外掛,例項化Prisma Client,將其分配給server.app,並新增一個擴充套件函式(可以看作是一個鉤子),它將在onPostStop事件上執行,該事件在伺服器的連線監聽器停止後被呼叫。
要註冊Prisma外掛,請在server.ts中匯入該外掛,並將其新增到傳遞給server.register呼叫的陣列中,如下所示:
如果您使用VSCode,您會在src/plugins/prisma.ts檔案中server.app.prisma = prisma下方看到一條紅色波浪線。這是您遇到的第一個型別錯誤。如果您沒有看到這條線,可以執行compile指令碼來執行TypeScript編譯器。
此錯誤的原因是您修改了server.app但沒有更新其型別。要解決此錯誤,請在prismaPlugin定義上方新增以下內容:
這將擴充套件模組並將PrismaClient型別分配給server.app.prisma屬性。
注意:有關為何需要模組增強的更多資訊,請檢視DefinitelyTyped程式碼庫中的此評論。
除了讓TypeScript編譯器“滿意”之外,這還將使server.app.prisma在應用程式中任何地方被訪問時都能正常工作自動補全功能。
檢查點:如果您再次執行npm run compile,應該不會發出任何錯誤。
做得好!您現在已經定義了兩個外掛,並使Prisma Client可供應用程式的其他部分使用。在下一步中,您將為使用者路由定義一個外掛。
定義一個依賴於Prisma外掛的使用者路由外掛
現在您將為使用者路由定義一個新的外掛。這個外掛需要利用您在Prisma外掛中定義的Prisma Client,以便它可以在使用者特定的路由處理程式中執行CRUD操作。
Hapi外掛有一個可選的dependencies屬性,可用於指示對其他外掛的依賴。指定後,Hapi將確保外掛以正確的順序載入。
首先,為使用者外掛建立一個新檔案src/plugins/users.ts。
向檔案中新增以下內容:
在這裡,您向dependencies屬性傳遞了一個數組,以確保Hapi首先載入Prisma外掛。
現在您可以在register函式中定義使用者特定的路由,因為您知道Prisma Client將可訪問。
最後,您需要匯入外掛並將其註冊到src/server.ts中,如下所示:
在下一步中,您將定義一個建立使用者的端點。
定義建立使用者路由
定義了使用者外掛後,您現在可以定義建立使用者的路由了。
建立使用者路由將使用POST HTTP方法和/users路徑。
首先,在src/plugins/users.ts的register函式內部新增以下server.route呼叫:
然後按如下方式定義createUserHandler函式:
在這裡,您從server.app物件(在Prisma外掛中分配)訪問prisma,並在prisma.user.create呼叫中使用請求有效負載將使用者儲存到資料庫中。
您應該會再次在訪問payload屬性的行下方看到一條紅色波浪線,表示型別錯誤。如果您沒有看到錯誤,請再次執行TypeScript編譯器。
這是因為payload的值是在執行時確定的,因此TypeScript編譯器無法知道它的型別。這可以透過型別斷言來解決。型別斷言是TypeScript中的一種機制,允許您覆蓋變數的推斷型別。TypeScript的型別斷言純粹是您告訴編譯器您比它更瞭解型別,就像這裡一樣。
為此,請為預期有效負載定義一個介面:
注意:TypeScript中的型別(Types)和介面(Interfaces)有許多相似之處。
然後新增型別斷言:
該外掛應如下所示:
向建立使用者路由新增驗證
在這一步中,您還將使用Joi新增有效負載驗證,以確保路由只處理具有正確資料的請求。
驗證可以被認為是執行時型別檢查。當使用TypeScript時,編譯器執行的型別檢查受限於編譯時可知的內容。由於使用者API輸入在編譯時無法得知,因此執行時驗證有助於處理此類情況。
為此,請按如下方式匯入Joi:
Joi允許您透過建立Joi驗證物件來定義驗證規則,該物件可以分配給路由處理程式,以便Hapi知道如何驗證有效負載。
在建立使用者端點中,您希望驗證使用者輸入是否符合您上面定義的型別。
相應的Joi驗證物件將如下所示:
接下來,您必須配置路由處理程式以使用驗證器物件userInputValidator。將以下內容新增到您的路由定義物件中:
為建立使用者路由建立測試
在這一步中,您將建立一個測試來驗證建立使用者邏輯。該測試將使用server.inject向POST /users端點發出請求,並檢查響應是否包含id欄位,從而驗證使用者已在資料庫中建立。
首先建立一個tests/users.tests.ts檔案,並新增以下內容:
該測試注入了一個帶有有效負載的請求,並斷言了statusCode以及響應中的id是一個數字。
注意:該測試透過確保每次測試執行時
既然您已經為成功建立使用者的“快樂路徑”編寫了測試,您將編寫另一個測試來驗證驗證邏輯。您將透過製作另一個帶有無效有效負載的請求來完成此操作,例如,省略所需的firstName欄位,如下所示:
檢查點:使用npm test命令執行測試,並驗證所有測試是否透過。
定義和測試獲取使用者路由
在這一步中,您將首先為獲取使用者端點定義一個測試,然後實現路由處理程式。
提醒一下,獲取使用者端點將具有GET /users/{userId}簽名。
先編寫測試再實現程式碼的實踐通常被稱為測試驅動開發。測試驅動開發可以透過提供一種快速機制來驗證更改的正確性,從而提高工作效率。
定義測試
首先,您將測試當用戶未找到時路由返回404的情況。
開啟users.test.ts檔案並新增以下測試:
第二個測試將測試“快樂路徑”——成功檢索到使用者。您將使用上一步中建立使用者測試中設定的userId變數。這將確保您獲取到一個現有使用者。新增以下測試:
由於您尚未定義路由,現在執行測試將導致測試失敗。下一步將是定義路由。
定義路由
轉到users.ts(使用者外掛)並將以下路由物件新增到server.route()呼叫中:
與您為建立使用者端點定義驗證規則的方式類似,在上面的路由定義中,您驗證了userId URL引數以確保傳遞的是數字。
接下來,按如下方式定義getUserHandler函式:
注意:當呼叫
findUnique時,如果未找到任何結果,Prisma將返回null。
在處理程式中,userId從請求引數中解析並用於Prisma Client查詢。如果找不到使用者,則返回404;否則,返回找到的使用者物件。
檢查點:使用npm test執行測試,並驗證所有測試是否透過。
定義和測試刪除使用者路由
在這一步中,您將定義一個刪除使用者端點的測試,然後實現路由處理程式。
刪除使用者端點將具有DELETE /users/{userId}簽名。
定義測試
首先,您將為路由的引數驗證編寫一個測試。將以下測試新增到users.test.ts中:
然後為刪除使用者邏輯新增另一個測試,在該測試中您將刪除在建立使用者測試中建立的使用者:
注意:204狀態響應碼錶示請求已成功,但響應沒有內容。
定義路由
轉到users.ts(使用者外掛)並將以下路由物件新增到server.route()呼叫中:
定義路由後,按如下方式定義deleteUserHandler函式:
檢查點:使用npm test執行測試,並驗證所有測試是否透過。
定義和測試更新使用者路由
在這一步中,您將定義一個更新使用者端點的測試,然後實現路由處理程式。
更新使用者端點將具有PUT /users/{userId}簽名。
為更新使用者路由編寫測試
首先,您將為路由的引數驗證編寫一個測試。將以下測試新增到users.test.ts中:
為更新使用者端點新增另一個測試,在該測試中您將更新使用者的firstName和lastName欄位(針對在建立使用者測試中建立的使用者):
定義更新使用者驗證規則
在這一步中,您將定義更新使用者路由。在驗證方面,該端點的有效負載不應要求任何特定欄位(與建立使用者端點不同,該端點要求email、firstName和lastName)。這將允許您使用該端點更新單個欄位,例如firstName。
要定義有效負載驗證,您可以使用userInputValidator Joi物件,但是,如果您還記得,其中一些欄位是必需的。
在更新使用者端點中,所有欄位都應該是可選的。Joi提供了一種使用tailor和alter方法建立相同Joi物件不同變體的方式。這在定義具有相似驗證規則的建立和更新路由時特別有用,同時保持程式碼DRY。
按如下方式更新已定義的userInputValidator:
更新建立使用者路由的有效負載驗證
現在您可以更新建立使用者路由定義,以便在src/plugins/users.ts(使用者外掛)中使用createUserValidator。
定義更新使用者路由
定義了更新驗證物件後,您現在可以定義更新使用者路由。轉到src/plugins/users.ts(使用者外掛)並將以下路由物件新增到server.route()呼叫中:
定義路由後,按如下方式定義updateUserHandler函式:
檢查點:使用npm test執行測試,並驗證所有測試是否透過。
總結與後續步驟
如果您已經讀到這裡,恭喜您。本文涵蓋了從REST概念到Hapi概念(如路由、外掛、外掛依賴、測試和驗證)的許多內容。
您為Hapi實現了一個Prisma外掛,使Prisma在您的應用程式中可用,並實現了利用它的路由。
此外,TypeScript在整個應用程式中幫助實現了自動補全和驗證型別的正確使用(與資料庫schema同步)。
本文涵蓋了所有端點的一個子集的實現。下一步,您可以按照相同的原則實現其他路由。
您可以在GitHub上找到完整的後端原始碼。
本文的重點是實現REST API,但是,驗證和測試等概念也適用於其他情況。
雖然Prisma旨在簡化關係資料庫的操作,但對底層資料庫有更深入的理解會很有幫助。
查閱Prisma資料指南,瞭解更多關於資料庫如何工作、如何選擇合適的資料庫以及如何在應用程式中充分利用資料庫的資訊。
在本系列的後續部分中,您將瞭解更多關於:
- 身份驗證:實現使用電子郵件和JWT的無密碼身份驗證。
- 持續整合:構建GitHub Actions流水線以自動化後端測試。
- 與外部API整合:使用事務性電子郵件API傳送電子郵件。
- 授權:為不同資源提供不同級別的訪問許可權。
- 部署
不要錯過下一篇文章!
訂閱Prisma新聞通訊