cd real-world-grading-app
npm install

注意:通過查看part-2分支,您將能夠從相同的起點跟蹤文章。

啟動 PostgreSQL

要啟動 PostgreSQL,請從該文件夾運行以下命令:real-world-grading-app

docker-compose up -d

注意:Docker 將使用docker-compose.yml文件啟動 PostgreSQL 容器。

構建 REST API

在深入研究實現之前,我們將介紹一些與 REST API 上下文相關的基本概念:

注意:REST 方法的主要目標之一是使用 HTTP 作為應用程序協議,以避免因堅持約定而重新發明輪子。

API 端點

API 將具有以下端點(HTTP 方法后跟路徑):

資源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/13userId13

上述端點已根據它們關聯的主模型/資源進行了分組。分類將有助于將代碼組織到單獨的模塊中,以實現可維護性。

在本文中,您將實現上述端點的子集(前 4 個),以說明不同 CRUD 操作的不同模式。完整的 API 將在 GitHub 存儲庫中提供。 這些終端節點應為大多數操作提供接口。雖然某些資源沒有用于刪除資源的終端節點,但可以稍后添加它們。

注意:在整篇文章中,endpoint 和 route 這兩個詞將互換使用。雖然它們指的是同一事物,但 endpoint 是 REST 上下文中使用的術語,而 route 是 HTTP 服務器上下文中使用的術語。

Hapi

該 API 將使用 Hapi 構建,這是一個Node.js框架,用于構建支持開箱即用驗證和測試的 HTTP 服務器。

Hapi由一個名為HTTP服務器的核心模塊和擴展核心功能的插件組成。在這個后端項目中,您還將使用以下Hapi插件:

要使用TypeScript開發Hapi應用,您需要添加Hapi和Joi的類型定義。這是必要的,因為Hapi是用JavaScript編寫的。通過添加這些類型定義,您將獲得豐富的自動完成功能,并允許TypeScript編譯器確保代碼的類型安全。

安裝以下軟件包:

npm install --save @hapi/boom @hapi/hapi @hapi/joi
npm install --save-dev @types/hapi__hapi @types/hapi__joi

創建服務器

您需要做的第一件事是創建一個 Happy 服務器,它將綁定到接口和端口。

將以下Hapi服務器添加到 src/server.ts

import Hapi from '@hapi/hapi'

const server: Hapi.Server = Hapi.server({
port: process.env.PORT || 3000,
host: process.env.HOST || 'localhost',
})

export async function start(): Promise<Hapi.Server> {
await server.start()
return server
}

process.on('unhandledRejection', err => {
console.log(err)
process.exit(1)
})

start()
.then(server => {
console.log(Server running on ${server.info.uri}) }) .catch(err => { console.log(err) })

首先,導入Hapi。然后初始化一個新的Hapi.server()(在Hapi.Server包中定義的類型為@types/hapi__hapi),其中包含連接細節,包括要監聽的端口號和主機信息。之后,您啟動服務器并記錄它正在運行。

要在開發期間本地運行服務器,請運行npm dev腳本,該腳本將使用ts-node-dev自動轉譯TypeScript代碼并在您進行更改時重新啟動服務器:npm run dev

npm run dev

> ts-node-dev --respawn ./src/server.ts

Using ts-node version 8.10.2, typescript version 3.9.6
Server running on http://localhost:3000

檢查點:如果您在瀏覽器中打開http://localhost:3000,您應該看到以下內容:{"statusCode":404,"error":"Not Found","message":"Not Found"}

恭喜,您已成功創建服務器。但是,目前服務器還沒有定義任何路由。接下來的步驟中,您將開始定義第一個路由。

定義路由

要添加路由,您需要在上一步中實例化的Hapi服務器對象上使用server.route()方法。在定義與業務邏輯相關的路由之前,建議先添加一個返回HTTP狀態碼200的/status端點。這個端點對于確保服務器正常運行非常有幫助。

為此,請在start文件夾中的server.ts文件頂部添加以下內容:

export async function start(): Promise<Hapi.Server> {
server.route({
method: 'GET',
path: '/',
handler: (_, h: Hapi.ResponseToolkit) => {
return h.response({ up: true }).code(200)
},
})
await server.start()
console.log(Server running on ${server.info.uri}) return server }

在這里,您定義了HTTP方法、路徑和返回對象{ up: true }的處理程序,最后將HTTP狀態代碼設置為200

檢查點:如果您在瀏覽器中打開http://localhost:3000,您應該看到以下內容:{"statusCode":404,"error":"Not Found","message":"Not Found"}

將路由移動到插件

在上一步中,您定義了狀態端點。由于該API將公開許多不同的端點,如果將它們全部定義在start函數中,將不利于維護。

Hapi 提供了插件的概念,這是一種將后端分解為獨立的業務邏輯單元的方法。使用插件是保持代碼模塊化的有效方式。在本步驟中,您將把上一步中定義的路由移動到一個插件中。

這需要兩個步驟:

  1. 在一個新的文件中定義插件。
  2. 在調用server.start()之前,將插件注冊到服務器。

定義插件

開始,在 src/ 中創建一個名為 plugins 的新文件夾:

mkdir src/plugins

status.ts 文件夾中創建一個名為 src/plugins/ 的新文件:

touch src/plugins/status.ts

并將以下內容添加到文件中:

import Hapi from '@hapi/hapi'

const plugin: Hapi.Plugin<undefined> = {
name: 'app/status',
register: async function(server: Hapi.Server) {
server.route({
method: 'GET',
path: '/',
handler: (_, h: Hapi.ResponseToolkit) => {
return h.response({ up: true }).code(200)
},
})
},
}

export default plugin

Hapi插件是一個包含name屬性和register函數的對象,通常用來封裝插件的邏輯。name屬性是插件的名稱,它是一個字符串,用作插件的唯一標識。

每個插件都可以通過標準的服務器接口來操作服務器。例如,在您之前創建的 app/status插件中,server 對象在 register 函數中被用來定義狀態路由。

注冊插件

要注冊插件,請返回server.ts并導入狀態插件,如下所示:

import status from './plugins/status'

start 函數中,將上一步中的route()調用替換為以下server.register()調用:

export async function start(): Promise<Hapi.Server> {
await server.register([status])
await server.start()
console.log(Server running on ${server.info.uri}) return server }

檢查點:如果您在瀏覽器中打開http://localhost:3000,您應該看到以下內容:{"statusCode":404,"error":"Not Found","message":"Not Found"}

恭喜,您已經成功創建了一個Hapi插件,它封裝了狀態端點的邏輯。

在下一步中,您將定義一個測試來測試 status 終端節點。

定義 status 端點的測試

為了測試狀態端點,您將使用Jest作為測試運行器,并使用Hapi的server.inject測試助手來模擬對服務器的HTTP請求。這將幫助您確認端點是否按預期正確實現。

將server.ts拆分為兩個文件

為了在測試中使用 server.inject 方法,您需要在插件注冊之后但在啟動服務器之前訪問 server 對象,以防止服務器在測試執行時監聽請求。為此,您需要對 server.ts 文件進行如下修改:

const server: Hapi.Server = Hapi.server({
port: process.env.PORT || 3000,
host: process.env.HOST || 'localhost',
})

export async function createServer(): Promise<Hapi.Server> {
await server.register([statusPlugin])
await server.initialize()

return server
}

export async function startServer(server: Hapi.Server): Promise<Hapi.Server> {
await server.start()
console.log(Server running on ${server.info.uri}) return server } process.on('unhandledRejection', err => { console.log(err) process.exit(1) })

您已經用兩個新的函數取代了原有的start函數

注意:Hapi的server.initialize()監聽服務器(啟動緩存,完成插件注冊),但不開始監聽連接端口。

現在,您可以在測試中導入 server.ts ,并使用 createServer() 來初始化服務器,然后調用 server.inject() 來模擬HTTP請求。

接下來,您需要為應用程序創建一個新的入口點,這個入口點將調用 createServer()startServer()

請創建一個新的文件 src/index.ts,并在其中添加以下內容:

import { createServer, startServer } from './server'

createServer()
.then(startServer)
.catch(err => {
console.log(err)
})

最后,更新 dev 中的 package.json 腳本,以啟動 src/index.ts 而不是 src/server.ts

- "dev": "ts-node-dev --respawn ./src/server.ts",
"dev": "ts-node-dev --respawn ./src/index.ts",

創建測試

要創建測試,請在項目的根目錄下創建一個名為tests的文件夾,并創建一個名為status.test.ts的文件,然后將以下內容添加到該文件中:

import { createServer } from '../src/server'
import Hapi from '@hapi/hapi'

describe('Status plugin', () => {
let server: Hapi.Server

beforeAll(async () => {
server = await createServer()
})

afterAll(async () => {
await server.stop()
})

test('status endpoint returns 200', async () => {
const res = await server.inject({
method: 'GET',
url: '/',
})
expect(res.statusCode).toEqual(200)
const response = JSON.parse(res.payload)
expect(response.up).toEqual(true)
})
})

在上述測試中,beforeAllafterAll被用作設置(setup)和清理(teardown)函數,分別用于創建和停止服務器。

接著,通過調用server.inject來模擬對根端點GET /的HTTP請求。然后,測試將斷言HTTP狀態碼和有效載荷,確保它們與處理程序的預期結果相匹配。

檢查點:運行npm test來執行測試,您應該看到以下輸出

PASS  tests/status.test.ts
Status plugin
? status endpoint returns 200 (9 ms)

Test Suites: 1 passed, 1 total
Tests: 1 passed, 1 total
Snapshots: 0 total
Time: 0.886 s, estimated 1 s
Ran all test suites.

恭喜,您已經成功創建了一個帶有路由的插件,并且對該路由進行了測試。

下一步中,您將定義一個Prisma插件,這樣您就可以在整個應用程序中訪問Prisma Client實例。

定義 Prisma 插件

類似于創建狀態插件的方式,您需要為Prisma插件創建一個新的文件src/plugins/prisma.ts

Prisma插件的目的是實例化Prisma客戶端,并通過server.app對象使其在整個應用程序中可用,并在服務器停止時斷開與數據庫的連接。server.app提供了一個安全的地方來存儲特定于服務器的運行時應用程序數據,這樣可以避免與框架內部發生潛在的沖突。只要服務器處于可訪問狀態,就可以訪問這些數據。

請將以下內容添加到src/plugins/prisma.ts文件中:

import { PrismaClient } from '@prisma/client'
import Hapi from '@hapi/hapi'

// plugin to instantiate Prisma Client
const prismaPlugin: Hapi.Plugin<null> = {
name: 'prisma',
register: async function(server: Hapi.Server) {
const prisma = new PrismaClient()

server.app.prisma = prisma

// Close DB connection after the server's connection listeners are stopped
// Related issue: https://github.com/hapijs/hapi/issues/2839
server.ext({
type: 'onPostStop',
method: async (server: Hapi.Server) => {
server.app.prisma.disconnect()
},
})
},
}

export default prismaPlugin

在這里,我們定義了一個插件來實例化Prisma Client,將其分配給server.app,并添加了一個擴展函數(可以看作是一個鉤子),該函數將在服務器的連接監聽器停止后調用的onPostStop事件上執行。

要注冊Prisma插件,請在server.ts中導入該插件,并將其添加到傳遞給server.register調用的數組中,如下所示:

await server.register([status, prisma])

如果您使用的是VSCode,您可能會在server.app.prisma = prisma這一行,位于src/plugins/prisma.ts文件中看到一條紅色的波浪線。這表明您遇到了第一個類型錯誤。如果您沒有看到這一行,您可以運行compile腳本來執行TypeScript編譯器。

npm run compile

src/plugins/prisma.ts:21:16 - error TS2339: Property 'prisma' does not exist on type 'ServerApplicationState'.

21 server.app.prisma = prisma

出現此錯誤的原因是您修改了server.app但沒有更新其類型。要解決此錯誤,請在prismaPlugin定義的頂部添加以下內容:

declare module '@hapi/hapi' {
interface ServerApplicationState {
prisma: PrismaClient
}
}

這將導入模塊并將PrismaClient類型分配給server.app.prisma屬性。

這樣做不僅能夠平息TypeScript編譯器的警告,還能使得在應用程序的其他部分中訪問server.app.prisma時,自動完成功能可以正常工作。

檢查站::如果再次運行npm run compile,應該不會出現錯誤。

干得好!您現在已經定義了兩個插件,并將Prisma Client使得應用程序的其他部分可以利用它。下一步中,您將為用戶路由定義一個插件。

為依賴于 Prisma 插件的用戶路由定義插件

現在,您將為用戶路由定義一個新的插件。這個插件需要使用您在Prisma插件中定義的Prisma Client,這樣它才能在特定于用戶的路由處理程序中執行CRUD操作。

Hapi插件有一個可選的dependencies屬性,可以用來聲明對其他插件的依賴。一旦指定,Hapi將確保這些插件按正確的順序加載。

首先,為users插件創建一個新文件src/plugins/users.ts

然后,將以下內容添加到該文件中:

import Hapi from '@hapi/hapi'

// plugin to instantiate Prisma Client
const usersPlugin = {
name: 'app/users',
dependencies: ['prisma'],
register: async function(server: Hapi.Server) {
// here you can use server.app.prisma
},
}
export default usersPlugin

在這里,您向dependencies屬性傳遞了一個數組,以確保Hapi首先加載Prisma插件。

現在,您可以在register函數中定義特定于用戶的路由,因為您可以確信Prisma Client將是可訪問的

最后,您需要導入插件并在src/server.ts中注冊它,如下所示:

await server.register([status, prisma])
await server.register([status, prisma, users])

在下一步中,您將定義一個 create user 端點。

定義 create user 路由

定義用戶插件后,您現在可以定義 create user 路由。

創建用戶路由將具有HTTP方法POST和路徑/users

開始,在server.route函數中的src/plugins/users.ts中添加以下register調用:

server.route([
{
method: 'POST',
path: '/users',
handler: createUserHandler,
},
])

然后定義createUserHandler函數如下:

async function createUserHandler(request: Hapi.Request, h: Hapi.ResponseToolkit) {
const { prisma } = request.server.app
const payload = request.payload

try {
const createdUser = await prisma.user.create({
data: {
firstName: payload.firstName,
lastName: payload.lastName,
email: payload.email,
social: JSON.stringify(payload.social),
},
select: {
id: true,
},
})
return h.response(createdUser).code(201)
} catch (err) {
console.log(err)
}
}

在這里,您從prisma對象(在Prisma插件中分配)訪問server.app,并在prisma.user.create調用中使用請求有效負載將用戶保存在數據庫中。

您可能會在訪問payload屬性的行下面再次看到一條紅色的波浪線,這表示存在類型錯誤。如果沒有看到錯誤,請再次運行TypeScript編譯器來檢查代碼:

npm run compile
src/plugins/users.ts:27:28 - error TS2339: Property 'firstName' does not exist on type 'string | object | Buffer | Readable'.
Property 'firstName' does not exist on type 'string'.

27 firstName: payload.firstName,


這是因為payload的值是在運行時確定的,所以TypeScript編譯器無法在編譯時知道它的類型。這個問題可以通過類型斷言來解決。類型斷言是TypeScript中的一種機制,它允許您重寫變量的推斷類型。在TypeScript中,類型斷言純粹是告訴編譯器,您比它更了解這個變量的類型。

為此,請為預期的有效負載定義一個接口:

interface UserInput {
firstName: string
lastName: string
email: string
social: {
facebook?: string
twitter?: string
github?: string
website?: string
}
}

注意:類型和接口在 TypeScript 中有許多相似之處。

然后添加類型斷言:

const payload = request.payload as UserInput

該插件應如下所示:

// plugin to instantiate Prisma Client
const usersPlugin = {
name: 'app/users',
dependencies: ['prisma'],
register: async function(server: Hapi.Server) {
server.route([
{
method: 'POST',
path: '/users',
handler: registerHandler,
},
])
},
}

export default usersPlugin

interface UserInput {
firstName: string
lastName: string
email: string
social: {
facebook?: string
twitter?: string
github?: string
website?: string
}
}

async function registerHandler(request: Hapi.Request, h: Hapi.ResponseToolkit) {
const { prisma } = request.server.app
const payload = request.payload as UserInput

try {
const createdUser = await prisma.user.create({
data: {
firstName: payload.firstName,
lastName: payload.lastName,
email: payload.email,
social: JSON.stringify(payload.social),
},
select: {
id: true,
},
})
return h.response(createdUser).code(201)
} catch (err) {
console.log(err)
}
}

將驗證添加到 create user 路由

在本步驟中,您將使用Joi為創建用戶路由添加有效載荷驗證,確保該路由只處理攜帶正確數據的請求。

可以將驗證看作是運行時的類型檢查。在使用TypeScript時,編譯器執行的類型檢查是基于編譯時已知的內容。由于在編譯時無法預知用戶API輸入的具體情況,因此運行時驗證對于處理這類情況非常有幫助。

為此,請按如下方式導入 Joi:

import Joi from '@hapi/joi'

Joi 使您能夠創建一個驗證對象來定義驗證規則,這個對象可以被賦予路由處理程序,從而讓 Hapi 知道如何驗證傳入的有效載荷。

在創建用戶端點的情況下,您需要確保用戶輸入的數據符合您之前定義的類型規范:

interface UserInput {
firstName: string
lastName: string
email: string
social: {
facebook?: string
twitter?: string
github?: string
website?: string
}
}

Joi 對應的驗證對象將如下所示:

const userInputValidator = Joi.object({
firstName: Joi.string().required(),
lastName: Joi.string().required(),
email: Joi.string()
.email()
.required(),
social: Joi.object({
facebook: Joi.string().optional(),
twitter: Joi.string().optional(),
github: Joi.string().optional(),
website: Joi.string().optional(),
}).optional(),
})

接下來,您必須配置路由處理程序以使用驗證器對象userInputValidator。將以下內容添加到路由定義對象:

{
method: 'POST',
path: '/users',
handler: registerHandler,
options: {
validate: {
payload: userInputValidator
}
},
}

為 create user 路由創建測試

在這一步中,您將編寫一個測試用例來驗證創建用戶的功能。這個測試將通過向POST /users端點發送請求來使用server.inject,并且會檢查響應是否包含id字段,以此來確認用戶是否已經成功創建在數據庫中。

首先,創建一個名為tests/users.tests.ts的文件,并在其中添加以下內容:

import { createServer } from '../src/server'
import Hapi from '@hapi/hapi'

describe('POST /users - create user', () => {
let server: Hapi.Server

beforeAll(async () => {
server = await createServer()
})

afterAll(async () => {
await server.stop()
})

let userId

test('create user', async () => {
const response = await server.inject({
method: 'POST',
url: '/users',
payload: {
firstName: 'test-first-name',
lastName: 'test-last-name',
email: test-${Date.now()}@prisma.io, social: { twitter: 'thisisalice', website: 'https://www.thisisalice.com' } } }) expect(response.statusCode).toEqual(201) userId = JSON.parse(response.payload)?.id expect(typeof userId === 'number').toBeTruthy() }) })

測試將注入一個帶有有效載荷的請求,并斷言響應中的statusCodeid是一個數字。

注:測試通過確保email在每次測試運行中都是唯一的,從而避免了唯一約束錯誤。

現在,您已經為Hapi路徑編寫了一個測試(成功創建了一個用戶),接下來您將編寫另一個測試來驗證驗證邏輯。您可以通過創建一個帶有無效載荷的請求來實現這一點,例如,省略必填字段firstName,如下所示:

test('create user validation', async () => {
const response = await server.inject({
method: 'POST',
url: '/users',
payload: {
lastName: 'test-last-name',
email: test-${Date.now()}@prisma.io, social: { twitter: 'thisisalice', website: 'https://www.thisisalice.com', }, }, }) console.log(response.payload) expect(response.statusCode).toEqual(400) })

檢查點:使用npm test命令運行測試,并驗證所有測試是否通過。

定義和測試 get user 路由

在該步驟中,您將首先為獲取用戶終端節點定義一個測試,然后實現路由處理程序。

提醒一下,獲取用戶端點將具有GET /users/{userId}的簽名。

先編寫測試,然后實現的做法通常稱為測試驅動開發(Test-Driven Development, TDD)。測試驅動開發可以通過提供一種快速驗證更改正確性的機制,從而提高工作效率。

定義測試

首先,您將測試當用戶未找到時路由返回404的情況。

打開users.test.ts文件,并添加以下測試用例:

test('get user returns 404 for non existant user', async () => {
const response = await server.inject({
method: 'GET',
url: '/users/9999',
})

expect(response.statusCode).toEqual(404)
})

第二個測試將針對成功路徑——即成功檢索到用戶的情況。您將使用在上一步創建用戶測試中設置的userId變量。這樣可以確保您獲取的是一個已存在的用戶。請添加以下測試:

test('get user returns user', async () => {
const response = await server.inject({
method: 'GET',
url: /users/${userId}, }) expect(response.statusCode).toEqual(200) const user = JSON.parse(response.payload) expect(user.id).toBe(userId) })

由于您尚未定義路由,因此現在運行測試將導致測試失敗。下一步將是定義路由。

定義路由

轉到users.ts(用戶插件)并將以下路由對象添加到server.route()調用:

server.route([
{
method: 'GET',
path: '/users/{userId}',
handler: getUserHandler,
options: {
validate: {
params: Joi.object({
userId: Joi.number().integer(),
}),
},
},
},
])

與為創建用戶端點定義驗證規則的方式類似,在上面的路由定義中,您需要驗證userId URL參數以確保它是一個數字。

接下來,請按照以下方式定義getUserHandler函數:

async function getUserHandler(request: Hapi.Request, h: Hapi.ResponseToolkit) {
const { prisma } = request.server.app
const userId = parseInt(request.params.userId, 10)

try {
const user = await prisma.user.findUnique({
where: {
id: userId,
},
})
if (!user) {
return h.response().code(404)
} else {
return h.response(user).code(200)
}
} catch (err) {
console.log(err)
return Boom.badImplementation()
}
}

注意:調用findUnique時,如果沒有找到結果,Prisma將返回null

在處理程序中,從請求參數中解析出userId,并將其用于Prisma Client查詢。如果找不到用戶,則返回404狀態碼;如果找到了用戶,則返回該用戶對象。

檢查點:使用npm test運行測試,并驗證所有測試均已通過。

定義和測試刪除用戶路由

在這一步中,您將首先為刪除用戶端點定義一個測試,然后實現路由處理程序。

刪除用戶端點將具有DELETE /users/{userId}的簽名。

定義測試

首先,您將為路由的參數驗證編寫一個測試。將以下測試添加到users.test.ts

test('delete user fails with invalid userId parameter', async () => {
const response = await server.inject({
method: 'DELETE',
url: /users/aa22, }) expect(response.statusCode).toEqual(400) })

然后為 delete user 邏輯添加另一個測試,您將在該邏輯中刪除在 create user 測試中創建的用戶:

test('delete user', async () => {
const response = await server.inject({
method: 'DELETE',
url: /users/${userId}, }) expect(response.statusCode).toEqual(204) })

注意:204 status 響應代碼表示請求成功,但響應沒有內容。

定義路由

轉到 users.ts(用戶插件)并將以下路由對象添加到server.route()調用:

server.route([
{
method: 'DELETE',
path: '/users/{userId}',
handler: deleteUserHandler,
options: {
validate: {
params: Joi.object({
userId: Joi.number().integer(),
}),
},
},
},
])

定義路由后,按如下方式定義deleteUserHandler

async function deleteUserHandler(request: Hapi.Request, h: Hapi.ResponseToolkit) {
const { prisma } = request.server.app
const userId = parseInt(request.params.userId, 10)

try {
await prisma.user.delete({
where: {
id: userId,
},
})
return h.response().code(204)
} catch (err) {
console.log(err)
return h.response().code(500)
}
}

檢查點:使用npm test運行測試,并驗證所有測試均已通過。

定義和測試更新用戶路由

在該步驟中,您將為 update user 端點定義一個測試,然后實施路由處理程序。

更新用戶端點將具有PUT /users/{userId}簽名。

為 update user 路由編寫測試

首先,您將為路由的參數驗證編寫一個測試。將以下測試添加到users.test.ts

test('update user fails with invalid userId parameter', async () => {
const response = await server.inject({
method: 'PUT',
url: /users/aa22, }) expect(response.statusCode).toEqual(400) })

為更新用戶端點添加一個新的測試,該測試將更新在創建用戶測試中創建的用戶的firstNamelastName字段:

test('update user', async () => {
const updatedFirstName = 'test-first-name-UPDATED'
const updatedLastName = 'test-last-name-UPDATED'

const response = await server.inject({
method: 'PUT',
url: /users/${userId}, payload: { firstName: updatedFirstName, lastName: updatedLastName, }, }) expect(response.statusCode).toEqual(200) const user = JSON.parse(response.payload) expect(user.firstName).toEqual(updatedFirstName) expect(user.lastName).toEqual(updatedLastName) })

定義更新用戶驗證規則

在本步驟中,您將為更新用戶路由定義驗證規則。與創建用戶端點不同,后者要求有效載荷中必須包含特定的字段(如emailfirstNamelastName),更新用戶端點的有效載荷不應強制要求任何特定字段。這樣的設計允許您僅更新單個字段,例如firstName

要定義有效載荷的驗證規則,您可以使用userInputValidator Joi對象但如果您還記得,某些字段在創建用戶時是必需的:

const userInputValidator = Joi.object({
firstName: Joi.string().required(),
lastName: Joi.string().required(),
email: Joi.string()
.email()
.required(),
social: Joi.object({
facebook: Joi.string().optional(),
twitter: Joi.string().optional(),
github: Joi.string().optional(),
website: Joi.string().optional(),
}).optional(),
})

在更新用戶端點中,所有字段都應該是可選的。Joi 提供了 tailoralter 方法,可以用來創建相同 Joi 對象的不同變體。這在定義具有相似驗證規則的創建和更新路由時特別有用,同時保持代碼的 DRY(Don’t Repeat Yourself)原則。

請按照以下方式更新已定義的 userInputValidator

const userInputValidator = Joi.object({
firstName: Joi.string().alter({
create: schema => schema.required(),
update: schema => schema.optional(),
}),
lastName: Joi.string().alter({
create: schema => schema.required(),
update: schema => schema.optional(),
}),
email: Joi.string()
.email()
.alter({
create: schema => schema.required(),
update: schema => schema.optional(),
}),
social: Joi.object({
facebook: Joi.string().optional(),
twitter: Joi.string().optional(),
github: Joi.string().optional(),
website: Joi.string().optional(),
}).optional(),
})

const createUserValidator = userInputValidator.tailor('create')
const updateUserValidator = userInputValidator.tailor('update')

更新 create user 路由的有效負載驗證

現在,您可以更新創建用戶路由定義以在createUserValidator(用戶插件)中使用src/plugins/users.ts

{
method: 'POST',
path: '/users',
handler: createUserHandler,
options: {
validate: {
- payload: userInputValidator,
payload: createUserValidator,
}
}
}

定義更新用戶路由

定義了更新的驗證對象后,現在可以定義更新用戶路由。轉到src/plugins/users.ts(用戶插件)并將以下路由對象添加到server.route()調用:

server.route([
{
method: 'PUT',
path: '/users/{userId}',
handler: updateUserHandler,
options: {
validate: {
params: Joi.object({
userId: Joi.number().integer(),
}),
payload: createUserValidator,
},
},
])

定義路由后,按如下方式定義updateUserHandler函數:

async function updateUserHandler(request: Hapi.Request, h: Hapi.ResponseToolkit) {
const { prisma } = request.server.app
const userId = parseInt(request.params.userId, 10)
const payload = request.payload as Partial<UserInput>

try {
const updatedUser = await prisma.user.update({
where: {
id: userId,
},
data: payload,
})
return h.response(updatedUser).code(200)
} catch (err) {
console.log(err)
return h.response().code(500)
}
}

檢查點:使用npm test運行測試,并驗證所有測試均已通過。

摘要和后續步驟

如果您已經完成了之前的步驟,那么恭喜您。在本文中,我們涵蓋了從REST概念的基礎出發,深入到一些有趣的主題,如路由、插件、插件依賴、測試和驗證。

您實現了一個Hapi的Prisma插件,使得Prisma在整個應用程序中可用,并實現了使用它的路由。

此外,TypeScript在整個應用程序中幫助自動完成并驗證類型(與數據庫架構同步)的正確使用。

本文介紹了所有端點子集的實現。接下來,您可以按照相同的原則來實現其他的路由。

您可以在GitHub上找到后端的完整源代碼。

雖然本文的重點在于實現REST API,但驗證和測試等概念同樣適用于其他情況。

盡管Prisma旨在簡化關系數據庫的使用,但深入了解底層數據庫也是很有幫助的。

查看Prisma的數據指南,詳細了解數據庫的工作原理、如何選擇適合的數據庫以及如何將數據庫與應用程序結合使用,以充分發揮其潛力。

在本系列的下一部分中,您將深入了解:

原文鏈接:https://www.prisma.io/blog/backend-prisma-typescript-orm-with-postgresql-rest-api-validation-dcba1ps7kip3

上一篇:

如何使用 Python 調用 SOAP API

下一篇:

設計和開發Web API的最佳實踐
#你可能也喜歡這些API文章!

我們有何不同?

API服務商零注冊

多API并行試用

數據驅動選型,提升決策效率

查看全部API→
??

熱門場景實測,選對API

#AI文本生成大模型API

對比大模型API的內容創意新穎性、情感共鳴力、商業轉化潛力

25個渠道
一鍵對比試用API 限時免費

#AI深度推理大模型API

對比大模型API的邏輯推理準確性、分析深度、可視化建議合理性

10個渠道
一鍵對比試用API 限時免費