鍵.png)
云原生 API 網(wǎng)關(guān) APISIX 入門教程
歡迎來到本系列教程的第三課,我們將探討如何使用 NestJS、Prisma 和 PostgreSQL 構(gòu)建 REST API。在這一課里,您將學(xué)習(xí)到如何在 NestJS 應(yīng)用中實施有效的錯誤處理機制。
在本系列教程的第一章中,您已經(jīng)成功創(chuàng)建了一個新的 NestJS 項目,并將其與 Prisma、PostgreSQL 數(shù)據(jù)庫以及 Swagger 進(jìn)行了集成。在此基礎(chǔ)上,您為博客應(yīng)用程序的后端構(gòu)建了一個基礎(chǔ)的 REST API。進(jìn)入第二章,您深入學(xué)習(xí)了如何進(jìn)行輸入驗證和轉(zhuǎn)換,以提升數(shù)據(jù)的準(zhǔn)確性和安全性。
在本章中,您將學(xué)習(xí)如何處理 NestJS 中的錯誤。您將了解兩種不同的策略:
在本教程中,您只需使用在第一章中構(gòu)建的 REST API 即可。無需完成第二章的學(xué)習(xí),您也可以直接開始本章的教程。
要學(xué)習(xí)本教程,您需要具備以下條件:
docker version
?命令來檢查 Docker 版本。如果您沒有 Unix shell(例如,您使用的是 Windows 計算機),您仍然可以繼續(xù)操作,但可能需要為您的計算機修改 shell 命令。
本教程的起點是本系列第一部分教程的結(jié)束點。它包含了一個使用 NestJS 構(gòu)建的基礎(chǔ) REST API。
本教程的起點位于 end-rest-api-part-1
分支。要開始學(xué)習(xí),請克隆存儲庫并檢出該分支。
git clone -b end-rest-api-part-1 git@github.com:prisma/blog-backend-rest-api-nestjs-prisma.git
現(xiàn)在,執(zhí)行以下操作以開始使用:
cd blog-backend-rest-api-nestjs-prisma
npm install
docker-compose up -d
npx prisma migrate dev
npm run start:dev
注意:步驟 4 還將生成 Prisma Client 并設(shè)定數(shù)據(jù)庫種子。
現(xiàn)在,您應(yīng)該能夠在以下位置訪問 API 文檔:http://localhost:3000/api/.
您克隆的存儲庫應(yīng)具有以下結(jié)構(gòu):
median
├── node_modules
├── prisma
│ ├── migrations
│ ├── schema.prisma
│ └── seed.ts
├── src
│ ├── app.controller.spec.ts
│ ├── app.controller.ts
│ ├── app.module.ts
│ ├── app.service.ts
│ ├── main.ts
│ ├── articles
│ └── prisma
├── test
│ ├── app.e2e-spec.ts
│ └── jest-e2e.json
├── README.md
├── .env
├── docker-compose.yml
├── nest-cli.json
├── package-lock.json
├── package.json
├── tsconfig.build.json
└── tsconfig.json
此存儲庫中值得注意的文件和目錄包括:
src
目錄:包含應(yīng)用程序的源代碼。該目錄下分為三個主要模塊:
app
模塊:位于 src
目錄的根目錄中,作為應(yīng)用程序的入口點,負(fù)責(zé)啟動 Web 服務(wù)器。prisma
模塊:包含 Prisma Client,即您的數(shù)據(jù)庫接口。articles
模塊:定義路由的端點和相關(guān)的業(yè)務(wù)邏輯。但注意,此處原描述中的“articles/articles”應(yīng)為筆誤,正常應(yīng)為 articles
目錄下的相關(guān)文件。schema.prisma
文件:定義數(shù)據(jù)庫架構(gòu),位于 prisma
目錄下(注意,原描述中將 prisma
錯誤地重復(fù)列出在 articles
下)。migrations
目錄:包含數(shù)據(jù)庫遷移的歷史記錄。seed.ts
文件:包含一個腳本,用于使用虛擬數(shù)據(jù)為開發(fā)數(shù)據(jù)庫設(shè)定初始數(shù)據(jù)。docker-compose.yml
文件:定義 PostgreSQL 數(shù)據(jù)庫的 Docker 映像。.env
文件:包含 PostgreSQL 數(shù)據(jù)庫的數(shù)據(jù)庫連接字符串。請注意,有關(guān)這些組件的更多詳細(xì)信息,請閱讀本教程系列的第一部分。
本節(jié)將指導(dǎo)您如何在應(yīng)用程序代碼中直接觸發(fā)異常。您將針對一個終端節(jié)點的問題進(jìn)行修復(fù)。目前,若向該終端節(jié)點傳遞無效的值,它不會返回 HTTP 狀態(tài)碼,而是直接返回錯誤信息。
例如,對于 GET 請求 /articles/:id
,如果傳遞的 ID(如 234235
)不存在,則會出現(xiàn)問題。
要解決此問題,您必須在 findOnearticles.controller.ts
文件中的方法做出更改。如果文章不存在,您應(yīng)該拋出一個 NotFoundException
,這是 NestJS 提供的內(nèi)置異常。
更新 findOnearticles.controller.ts
文件中的相關(guān)方法。
// src/articles/articles.controller.ts
import {
Controller,
Get,
Post,
Body,
Patch,
Param,
Delete,
NotFoundException,
} from '@nestjs/common';
@Get(':id')
@ApiOkResponse({ type: ArticleEntity })
findOne(@Param('id') id: string) {
return this.articlesService.findOne(+id);
async findOne(@Param('id') id: string) {
const article = await this.articlesService.findOne(+id);
if (!article) {
throw new NotFoundException(`Article with ${id} does not exist.`);
}
return article;
}
如果您再次發(fā)出相同的請求,您應(yīng)該會收到一條用戶友好的錯誤消息:
在上一節(jié)中,您學(xué)會了如何檢測錯誤狀態(tài)并手動拋出異常。然而,在許多情況下,應(yīng)用程序代碼會自動生成異常。此時,您應(yīng)該捕獲這些異常并向用戶返回合適的 HTTP 錯誤響應(yīng)。
盡管您可以在每個控制器中單獨處理異常,但這并非最佳實踐,原因如下:
為了解決這些問題,NestJS 提供了一個異常層,用于處理整個應(yīng)用程序中未捕獲的異常。在 NestJS 中,您可以創(chuàng)建異常過濾器來定義如何響應(yīng)應(yīng)用程序內(nèi)部拋出的不同類型的異常。
NestJS 支持全局異常過濾器,它可以捕獲所有未處理的異常。為了理解全局異常過濾器的工作原理,我們來看一個示例。請向?/articles
?終端節(jié)點發(fā)送 POST 請求,并觀察其響應(yīng)。
{
"title": "Let’s build a REST API with NestJS and Prisma.",
"description": "NestJS Series announcement.",
"body": "NestJS is one of the hottest Node.js frameworks around. In this series, you will learn how to build a backend REST API with NestJS, Prisma, PostgreSQL and Swagger.",
"published": true
}
第一個請求將會成功,但第二個請求會因為您已經(jīng)創(chuàng)建了具有相同標(biāo)題(title)的文章而失敗。您將會收到一個與標(biāo)題相關(guān)的錯誤。
{
"statusCode": 500,
"message": "Internal server error"
}
如果您查看運行 NestJS 服務(wù)器的終端窗口,您應(yīng)該會看到以下錯誤:
[Nest] 6803 - 12/06/2022, 3:25:40 PM ERROR [ExceptionsHandler]
Invalid `this.prisma.article.create()` invocation in
/Users/tasinishmam/my-code/median/src/articles/articles.service.ts:11:32
8 constructor(private prisma: PrismaService) {}
9
10 create(createArticleDto: CreateArticleDto) {
→ 11 return this.prisma.article.create(
Unique constraint failed on the fields: (`title`)
Error:
Invalid `this.prisma.article.create()` invocation in
/Users/tasinishmam/my-code/median/src/articles/articles.service.ts:11:32
8 constructor(private prisma: PrismaService) {}
9
10 create(createArticleDto: CreateArticleDto) {
→ 11 return this.prisma.article.create(
Unique constraint failed on the fields: (`title`)
從日志中,您可以觀察到 Prisma Client 因為某個字段觸發(fā)了唯一性約束驗證錯誤,該字段在 Prisma 架構(gòu)中被標(biāo)記為唯一。引發(fā)的異常類型是?PrismaClientKnownRequestError
,并且這個異常在 Prisma 的命名空間級別被導(dǎo)出。具體地說,是因為?title
?字段的唯一性約束導(dǎo)致了這個問題。
由于這個異常不是由您的應(yīng)用程序直接捕獲和處理的,因此它會被內(nèi)置的全局異常過濾器自動捕獲。然而,這個全局異常過濾器默認(rèn)生成的是 HTTP “Internal Server Error”(500)響應(yīng)。
在本節(jié)中,您將創(chuàng)建一個自定義的異常過濾器來處理上述類型的異常。這個自定義過濾器將能夠捕獲所有類型的異常,并且能夠為用戶返回清晰、友好的錯誤消息,特別是針對?PrismaClientKnownRequestError
?這類異常。
首先使用 Nest CLI 生成過濾器類:
npx nest generate filter prisma-client-exception
這將創(chuàng)建一個名為 src/prisma-client-exception.filter.ts
的新文件。
// src/prisma-client-exception.filter.ts
import { ArgumentsHost, Catch, ExceptionFilter } from '@nestjs/common';
@Catch()
export class PrismaClientExceptionFilter<T> implements ExceptionFilter {
catch(exception: T, host: ArgumentsHost) {}
}
注意:請注意,已創(chuàng)建了一個名為
prisma-client-exception.filter.spec.ts
的測試文件。目前,您可以暫時忽略這個文件。
由于 prisma-client-exception.filter.ts
中的方法實現(xiàn)為空,您可能會收到來自 ESLint 的錯誤提示。為了解決這個問題,您需要更新該方法以實現(xiàn)一個捕獲 Prisma 客戶端異常的過濾器。這里假設(shè)您已經(jīng)有了相關(guān)的實現(xiàn)計劃或代碼框架。
// src/prisma-client-exception.filter.ts
import { ArgumentsHost, Catch } from '@nestjs/common';
import { BaseExceptionFilter } from '@nestjs/core';
import { Prisma } from '@prisma/client';
@Catch(Prisma.PrismaClientKnownRequestError) // 1
export class PrismaClientExceptionFilter extends BaseExceptionFilter { // 2
catch(exception: Prisma.PrismaClientKnownRequestError, host: ArgumentsHost) {
console.error(exception.message); // 3
// default 500 error code
super.catch(exception, host);
}
}
在這里,您進(jìn)行了以下更改:
PrismaClientKnownRequestError
?類型的異常,您將其添加到了?@Catch(PrismaClientKnownRequestError)
?裝飾器中。BaseExceptionFilter
類,該類為向用戶返回“Internal server error”響應(yīng)的方法提供了默認(rèn)實現(xiàn)。您可以在 NestJS 文檔中了解更多相關(guān)信息。console.error
語句,用于將錯誤消息記錄到控制臺,這對于調(diào)試目的非常有用。由于 Prisma 會引發(fā)許多不同類型的錯誤,您需要弄清楚如何從捕獲的異常中提取錯誤代碼。異常對象具有一個包含錯誤代碼的屬性,您可以在 Prisma 錯誤消息參考中找到所有錯誤代碼的列表。
您要查找的錯誤代碼是?P2002
,它通常發(fā)生在唯一約束沖突中。接下來,您將更新?catch
?方法,以便在捕獲到此錯誤時引發(fā)一個帶有 HTTP 409 Conflict 狀態(tài)碼的響應(yīng),并向用戶提供自定義錯誤消息。
更新您的異常過濾器實現(xiàn),如下所示:
//src/prisma-client-exception.filter.ts
import { ArgumentsHost, Catch, HttpStatus } from '@nestjs/common';
import { BaseExceptionFilter } from '@nestjs/core';
import { Prisma } from '@prisma/client';
import { Response } from 'express';
@Catch(Prisma.PrismaClientKnownRequestError)
export class PrismaClientExceptionFilter extends BaseExceptionFilter {
catch(exception: Prisma.PrismaClientKnownRequestError, host: ArgumentsHost) {
console.error(exception.message);
const ctx = host.switchToHttp();
const response = ctx.getResponse<Response>();
const message = exception.message.replace(/\n/g, '');
switch (exception.code) {
case 'P2002': {
const status = HttpStatus.CONFLICT;
response.status(status).json({
statusCode: status,
message: message,
});
break;
}
default:
// default 500 error code
super.catch(exception, host);
break;
}
}
}
在這里,您將訪問底層框架對象并直接修改響應(yīng)。默認(rèn)情況下,express 是 NestJS 在后臺使用的 HTTP 框架。對于除 之外的任何異常代碼,您將發(fā)送默認(rèn)的 “Internal server error” 響應(yīng)。
注意:對于生產(chǎn)應(yīng)用程序,請注意不要在錯誤消息中向用戶泄露任何敏感信息。
要使?PrismaClientExceptionFilter
?生效,您需要將其應(yīng)用于適當(dāng)?shù)姆秶.惓_^濾器的應(yīng)用范圍可以是單個路由(方法級)、整個控制器(控制器級)或整個應(yīng)用程序(全局級)。
為了將異常過濾器應(yīng)用于整個應(yīng)用程序,請更新 main.ts
文件。
// src/main.ts
import { HttpAdapterHost, NestFactory } from '@nestjs/core';
import { AppModule } from './app.module';
import { SwaggerModule, DocumentBuilder } from '@nestjs/swagger';
import { PrismaClientExceptionFilter } from './prisma-client-exception.filter';
async function bootstrap() {
const app = await NestFactory.create(AppModule);
const config = new DocumentBuilder()
.setTitle('Median')
.setDescription('The Median API description')
.setVersion('0.1')
.build();
const document = SwaggerModule.createDocument(app, config);
SwaggerModule.setup('api', app, document);
const { httpAdapter } = app.get(HttpAdapterHost);
app.useGlobalFilters(new PrismaClientExceptionFilter(httpAdapter));
await app.listen(3000);
}
bootstrap();
現(xiàn)在,嘗試向 /articles
終端節(jié)點再次發(fā)出相同的 POST 請求。
{
"title": "Let’s build a REST API with NestJS and Prisma.",
"description": "NestJS Series announcement.",
"body": "NestJS is one of the hottest Node.js frameworks around. In this series, you will learn how to build a backend REST API with NestJS, Prisma, PostgreSQL and Swagger.",
"published": true
}
這次,您將收到一個更用戶友好的錯誤消息:
{
"statusCode": 409,
"message": "Invalid `this.prisma.article.create()` invocation in /Users/tasinishmam/my-code/median/src/articles/articles.service.ts:11:32 8 constructor(private prisma: PrismaService) {} 9 10 create(createArticleDto: CreateArticleDto) {→ 11 return this.prisma.article.create(Unique constraint failed on the fields: (`title`)"
}
由于?PrismaClientExceptionFilter
?是一個全局過濾器,因此它能夠為應(yīng)用程序中的所有路由處理?PrismaClientKnownRequestError
?這種特定類型的錯誤。
我建議您進(jìn)一步擴展異常過濾器的實現(xiàn),以處理其他類型的錯誤。例如,您可以添加一個分支來處理錯誤代碼 P2025
,這個錯誤代碼通常在數(shù)據(jù)庫中找不到記錄時出現(xiàn)。對于這種情況,您應(yīng)該返回 HttpStatus.NOT_FOUND
狀態(tài)碼。這對于 PATCH /articles/:id
和 DELETE /articles/:id
端點特別有用,因為這些操作通常依賴于特定記錄的存在。
到目前為止,您已經(jīng)了解了在 NestJS 應(yīng)用程序中手動處理 Prisma 異常的不同技術(shù)。有一個用于將 Prisma 與 NestJS 一起使用的專用包,稱為nestjs-prisma您還可以使用它來處理 Prisma 異常。此包是一個很好的考慮選擇,因為它刪除了大量樣板代碼。
有關(guān)如何安裝和使用 nestjs-prisma
包的詳細(xì)說明,請參考其官方文檔。使用此包時,您無需手動創(chuàng)建與 Prisma 相關(guān)的單獨模塊和服務(wù),因為這些都會由包自動為您生成。
在?nestjs-prisma
?文檔的“Exception Filter”部分,您可以了解到如何使用該包來處理 Prisma 異常。在本教程的后續(xù)章節(jié)中,我們將更深入地介紹這個軟件包。
祝賀!在本教程中,您獲取了一個現(xiàn)有的 NestJS 應(yīng)用程序,并學(xué)習(xí)了如何集成錯誤處理。您學(xué)習(xí)了兩種不同的錯誤處理方法:直接在應(yīng)用程序代碼中處理和創(chuàng)建異常過濾器。
在本章中,您特別學(xué)習(xí)了如何處理由 Prisma 引發(fā)的錯誤。但請注意,這些技術(shù)不僅適用于 Prisma,它們同樣可以用于處理應(yīng)用程序中的其他任何類型錯誤。
您可以在 end-error-handling-part-3
分支上找到本教程的結(jié)束點。如果您在學(xué)習(xí)過程中遇到問題,請隨時在存儲庫中提出疑問或提交 PR(Pull Request)。當(dāng)然,您也可以直接在 Twitter 上與我聯(lián)系。
原文鏈接:https://www.prisma.io/blog/nestjs-prisma-error-handling-7D056s1kOop2