
14個文本轉圖像AI API
cd real-world-grading-app
npm install
注意:通過查看
part-2
分支,您將能夠從相同的起點跟蹤文章。
要啟動 PostgreSQL,請從該文件夾運行以下命令:real-world-grading-app
docker-compose up -d
注意:Docker 將使用
docker-compose.yml
文件啟動 PostgreSQL 容器。
在深入研究實現之前,我們將介紹一些與 REST API 上下文相關的基本概念:
/users/
,用于訪問用戶端點。路徑確定用于訪問端點的URL,例如www.myapi.com/users/
。GET
、POST
和DELETE
。HTTP方法將確定端點公開的操作類型,例如GET /users
端點將允許獲取用戶,而POST /users
端點將允許創建用戶。201
,當消費者輸入驗證失敗時為400
。注意:REST 方法的主要目標之一是使用 HTTP 作為應用程序協議,以避免因堅持約定而重新發明輪子。
API 將具有以下端點(HTTP 方法后跟路徑):
資源 | HTTP 方法 | 路線 | 描述 |
---|---|---|---|
User | POST | /users | 創建用戶(并可選擇與課程關聯) |
User | GET | /users/{userId} | 獲取用戶 |
User | PUT | /users/{userId} | 更新用戶 |
User | DELETE | /users/{userId} | 刪除用戶 |
User | GET | /users | 獲取用戶 |
CourseEnrollment | GET | /users/{userId}/courses | 獲取用戶的課程中注冊 |
CourseEnrollment | POST | /users/{userId}/courses | 將用戶注冊到課程(作為學生或教師) |
CourseEnrollment | DELETE | /users/{userId}/courses/{courseId} | 刪除用戶對課程的注冊 |
Course | POST | /courses | 創建課程 |
Course | GET | /courses | 獲取課程 |
Course | GET | /courses/{courseId} | 獲取課程 |
Course | PUT | /courses/{courseId} | 更新課程 |
Course | DELETE | /courses/{courseId} | 刪除課程 |
Test | POST | /courses/{courseId}/tests | 為課程創建測試 |
Test | GET | /courses/tests/{testId} | 進行測試 |
Test | PUT | /courses/tests/{testId} | 更新測試 |
Test | DELETE | /courses/tests/{testId} | 刪除測試 |
Test Result | GET | /users/{userId}/test-results | 獲取用戶的測試結果 |
Test Result | POST | /courses/tests/{testId}/test-results | 為與用戶關聯的測試創建測試結果 |
Test Result | GET | /courses/tests/{testId}/test-results | 獲取測試的多個測試結果 |
Test Result | PUT | /courses/tests/test-results/{testResultId} | 更新測試結果(與用戶和測試關聯) |
Test Result | DELETE | /courses/tests/test-results/{testResultId} | 刪除測試結果 |
注:包含參數的路徑在
{}
中,例如{userId}
表示URL中插入的變量,例如在www.myapi.com/users/13
中userId
是13
。
上述端點已根據它們關聯的主模型/資源進行了分組。分類將有助于將代碼組織到單獨的模塊中,以實現可維護性。
在本文中,您將實現上述端點的子集(前 4 個),以說明不同 CRUD 操作的不同模式。完整的 API 將在 GitHub 存儲庫中提供。 這些終端節點應為大多數操作提供接口。雖然某些資源沒有用于刪除資源的終端節點,但可以稍后添加它們。
注意:在整篇文章中,endpoint 和 route 這兩個詞將互換使用。雖然它們指的是同一事物,但 endpoint 是 REST 上下文中使用的術語,而 route 是 HTTP 服務器上下文中使用的術語。
該 API 將使用 Hapi 構建,這是一個Node.js框架,用于構建支持開箱即用驗證和測試的 HTTP 服務器。
Hapi由一個名為HTTP服務器的核心模塊和擴展核心功能的插件組成。在這個后端項目中,您還將使用以下Hapi插件:
@hapi/hapi
:Hapi框架的核心模塊。@hapi/joi
:用于聲明性輸入驗證@hapi/boom
:用于 HTTP 友好的錯誤對象要使用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 提供了插件的概念,這是一種將后端分解為獨立的業務邏輯單元的方法。使用插件是保持代碼模塊化的有效方式。在本步驟中,您將把上一步中定義的路由移動到一個插件中。
這需要兩個步驟:
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 終端節點。
為了測試狀態端點,您將使用Jest作為測試運行器,并使用Hapi的server.inject
測試助手來模擬對服務器的HTTP請求。這將幫助您確認端點是否按預期正確實現。
為了在測試中使用 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
函數:
createServer()
:注冊插件并初始化服務器startServer()
:啟動服務器注意: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)
})
})
在上述測試中,beforeAll
和afterAll
被用作設置(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插件創建一個新的文件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 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 路由。
創建用戶路由將具有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)
}
}
在本步驟中,您將使用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
}
},
}
在這一步中,您將編寫一個測試用例來驗證創建用戶的功能。這個測試將通過向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()
})
})
測試將注入一個帶有有效載荷的請求,并斷言響應中的statusCode
和id
是一個數字。
注:測試通過確保
現在,您已經為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 /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}
簽名。
首先,您將為路由的參數驗證編寫一個測試。將以下測試添加到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)
})
為更新用戶端點添加一個新的測試,該測試將更新在創建用戶測試中創建的用戶的firstName
和lastName
字段:
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)
})
在本步驟中,您將為更新用戶路由定義驗證規則。與創建用戶端點不同,后者要求有效載荷中必須包含特定的字段(如email
、firstName
和lastName
),更新用戶端點的有效載荷不應強制要求任何特定字段。這樣的設計允許您僅更新單個字段,例如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 提供了 tailor
和 alter
方法,可以用來創建相同 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')
現在,您可以更新創建用戶路由定義以在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的數據指南,詳細了解數據庫的工作原理、如何選擇適合的數據庫以及如何將數據庫與應用程序結合使用,以充分發揮其潛力。
在本系列的下一部分中,您將深入了解: