所有文章 > 如何集成API > 使用 NestJS 和 Prisma 构建 REST API:错误处理

使用 NestJS 和 Prisma 构建 REST API:错误处理

欢迎来到本系列教程的第三课,我们将探讨如何使用 NestJS、Prisma 和 PostgreSQL 构建 REST API。在这一课里,您将学习到如何在 NestJS 应用中实施有效的错误处理机制。

使用 NestJS 和 Prisma 构建 REST API:错误处理

介绍

在本系列教程的第一章中,您已经成功创建了一个新的 NestJS 项目,并将其与 Prisma、PostgreSQL 数据库以及 Swagger 进行了集成。在此基础上,您为博客应用程序的后端构建了一个基础的 REST API。进入第二章,您深入学习了如何进行输入验证和转换,以提升数据的准确性和安全性。

在本章中,您将学习如何处理 NestJS 中的错误。您将了解两种不同的策略:

  1. 首先,您将学习如何在 API 的控制器内直接在应用程序代码中检测和抛出错误。
  2. 接下来,您将学习如何使用异常过滤器来处理整个应用程序中未处理的异常。

在本教程中,您只需使用在第一章中构建的 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-rest-api-part-1 分支。要开始学习,请克隆存储库并检出该分支。

git clone -b end-rest-api-part-1 git@github.com:prisma/blog-backend-rest-api-nestjs-prisma.git

现在,执行以下操作以开始使用:

  1. 导航到克隆的目录:
cd blog-backend-rest-api-nestjs-prisma
  1. 安装依赖项:
npm install
  1. 使用 Docker 启动 PostgreSQL 数据库:
docker-compose up -d
  1. 应用数据库迁移:
npx prisma migrate dev
  1. 启动项目:
npm run start:dev

注意:步骤 4 还将生成 Prisma Client 并设定数据库种子。

现在,您应该能够在以下位置访问 API 文档:http://localhost:3000/api/.

项目结构和文件

您克隆的存储库应具有以下结构:

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 目录:包含应用程序的源代码。该目录下分为三个主要模块:
    • app 模块:位于 src 目录的根目录中,作为应用程序的入口点,负责启动 Web 服务器。
    • prisma 模块:包含 Prisma Client,即您的数据库接口。
    • articles 模块:定义路由的端点和相关的业务逻辑。但注意,此处原描述中的“articles/articles”应为笔误,正常应为 articles 目录下的相关文件。
  • schema.prisma 文件:定义数据库架构,位于 prisma 目录下(注意,原描述中将 prisma 错误地重复列出在 articles 下)。
  • migrations 目录:包含数据库迁移的历史记录。
  • seed.ts 文件:包含一个脚本,用于使用虚拟数据为开发数据库设定初始数据。
  • docker-compose.yml 文件:定义 PostgreSQL 数据库的 Docker 映像。
  • .env 文件:包含 PostgreSQL 数据库的数据库连接字符串。

请注意,有关这些组件的更多详细信息,请阅读本教程系列的第一部分。

直接检测并引发异常

本节将指导您如何在应用程序代码中直接触发异常。您将针对一个终端节点的问题进行修复。目前,若向该终端节点传递无效的值,它不会返回 HTTP 状态码,而是直接返回错误信息。

例如,对于 GET 请求 /articles/:id,如果传递的 ID(如 234235)不存在,则会出现问题。

请求不存在的文章会返回 HTTP 200

要解决此问题,您必须在 findOnearticles.controller.ts 文件中的方法做出更改。如果文章不存在,您应该抛出一个 NotFoundException,这是 NestJS 提供的内置异常。

更新 findOnearticles.controller.ts 文件中的相关方法。

// 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;
}

如果您再次发出相同的请求,您应该会收到一条用户友好的错误消息:

请求不存在的文章会返回 HTTP 404

使用异常筛选器处理异常

专用例外层的优点

在上一节中,您学会了如何检测错误状态并手动抛出异常。然而,在许多情况下,应用程序代码会自动生成异常。此时,您应该捕获这些异常并向用户返回合适的 HTTP 错误响应。

尽管您可以在每个控制器中单独处理异常,但这并非最佳实践,原因如下:

  • 它将用大量错误处理代码弄乱您的核心应用程序逻辑。
  • 许多终端节点可能会遇到类似的错误,例如资源未找到。如果每个控制器都单独处理这些错误,将会导致大量重复的代码。
  • 由于错误处理逻辑分散在多个位置,因此很难对其进行更改。

为了解决这些问题,NestJS 提供了一个异常层,用于处理整个应用程序中未捕获的异常。在 NestJS 中,您可以创建异常过滤器来定义如何响应应用程序内部抛出的不同类型的异常。

NestJS 全局异常过滤器

NestJS 支持全局异常过滤器,它可以捕获所有未处理的异常。为了理解全局异常过滤器的工作原理,我们来看一个示例。请向 /articles 终端节点发送 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
}

第一个请求将会成功,但第二个请求会因为您已经创建了具有相同标题(title)的文章而失败。您将会收到一个与标题相关的错误。

{
"statusCode": 500,
"message": "Internal server error"
}

如果您查看运行 NestJS 服务器的终端窗口,您应该会看到以下错误:

[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 因为某个字段触发了唯一性约束验证错误,该字段在 Prisma 架构中被标记为唯一。引发的异常类型是 PrismaClientKnownRequestError,并且这个异常在 Prisma 的命名空间级别被导出。具体地说,是因为 title 字段的唯一性约束导致了这个问题。

由于这个异常不是由您的应用程序直接捕获和处理的,因此它会被内置的全局异常过滤器自动捕获。然而,这个全局异常过滤器默认生成的是 HTTP “Internal Server Error”(500)响应。

创建手动异常筛选条件

在本节中,您将创建一个自定义的异常过滤器来处理上述类型的异常。这个自定义过滤器将能够捕获所有类型的异常,并且能够为用户返回清晰、友好的错误消息,特别是针对 PrismaClientKnownRequestError 这类异常。

首先使用 Nest CLI 生成过滤器类:

npx nest generate filter prisma-client-exception

这将创建一个名为 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) {}
}

注意:请注意,已创建了一个名为 prisma-client-exception.filter.spec.ts 的测试文件。目前,您可以暂时忽略这个文件。

由于 prisma-client-exception.filter.ts 中的方法实现为空,您可能会收到来自 ESLint 的错误提示。为了解决这个问题,您需要更新该方法以实现一个捕获 Prisma 客户端异常的过滤器。这里假设您已经有了相关的实现计划或代码框架。

// 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);
}
}

在这里,您进行了以下更改:

  1. 为了确保此过滤器能够捕获 PrismaClientKnownRequestError 类型的异常,您将其添加到了 @Catch(PrismaClientKnownRequestError) 装饰器中。
  2. 异常过滤器扩展了 NestJS 核心包中的 BaseExceptionFilter 类,该类为向用户返回“Internal server error”响应的方法提供了默认实现。您可以在 NestJS 文档中了解更多相关信息。
  3. 您添加了一个 console.error 语句,用于将错误消息记录到控制台,这对于调试目的非常有用。

由于 Prisma 会引发许多不同类型的错误,您需要弄清楚如何从捕获的异常中提取错误代码。异常对象具有一个包含错误代码的属性,您可以在 Prisma 错误消息参考中找到所有错误代码的列表。

您要查找的错误代码是 P2002,它通常发生在唯一约束冲突中。接下来,您将更新 catch 方法,以便在捕获到此错误时引发一个带有 HTTP 409 Conflict 状态码的响应,并向用户提供自定义错误消息。

更新您的异常过滤器实现,如下所示:

//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;
}
}
}

在这里,您将访问底层框架对象并直接修改响应。默认情况下,express 是 NestJS 在后台使用的 HTTP 框架。对于除 之外的任何异常代码,您将发送默认的 “Internal server error” 响应。

注意:对于生产应用程序,请注意不要在错误消息中向用户泄露任何敏感信息。

将异常筛选器应用于应用程序

要使 PrismaClientExceptionFilter 生效,您需要将其应用于适当的范围。异常过滤器的应用范围可以是单个路由(方法级)、整个控制器(控制器级)或整个应用程序(全局级)。

为了将异常过滤器应用于整个应用程序,请更新 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();

现在,尝试向 /articles 终端节点再次发出相同的 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 是一个全局过滤器,因此它能够为应用程序中的所有路由处理 PrismaClientKnownRequestError 这种特定类型的错误。

我建议您进一步扩展异常过滤器的实现,以处理其他类型的错误。例如,您可以添加一个分支来处理错误代码 P2025,这个错误代码通常在数据库中找不到记录时出现。对于这种情况,您应该返回 HttpStatus.NOT_FOUND 状态码。这对于 PATCH /articles/:id 和 DELETE /articles/:id 端点特别有用,因为这些操作通常依赖于特定记录的存在。

使用nest js-prime包处理 Prisma 异常

到目前为止,您已经了解了在 NestJS 应用程序中手动处理 Prisma 异常的不同技术。有一个用于将 Prisma 与 NestJS 一起使用的专用包,称为nestjs-prisma您还可以使用它来处理 Prisma 异常。此包是一个很好的考虑选择,因为它删除了大量样板代码。

有关如何安装和使用 nestjs-prisma 包的详细说明,请参考其官方文档。使用此包时,您无需手动创建与 Prisma 相关的单独模块和服务,因为这些都会由包自动为您生成。

在 nestjs-prisma 文档的“Exception Filter”部分,您可以了解到如何使用该包来处理 Prisma 异常。在本教程的后续章节中,我们将更深入地介绍这个软件包。

总结和结束语

祝贺!在本教程中,您获取了一个现有的 NestJS 应用程序,并学习了如何集成错误处理。您学习了两种不同的错误处理方法:直接在应用程序代码中处理和创建异常过滤器。

在本章中,您特别学习了如何处理由 Prisma 引发的错误。但请注意,这些技术不仅适用于 Prisma,它们同样可以用于处理应用程序中的其他任何类型错误。

您可以在 end-error-handling-part-3 分支上找到本教程的结束点。如果您在学习过程中遇到问题,请随时在存储库中提出疑问或提交 PR(Pull Request)。当然,您也可以直接在 Twitter 上与我联系。

原文链接:https://www.prisma.io/blog/nestjs-prisma-error-handling-7D056s1kOop2

#你可能也喜欢这些API文章!