NestJS + Prisma 构建 REST API 系列教程(三):错误处理
欢迎来到 NestJS、Prisma 和 PostgresQL 构建 REST API 系列的第三篇教程。在本篇教程中,你将学习如何在 NestJS 程序中实施错误处理。
如果没有看过本系列的前两篇文章,可以移步: 使用 NestJS + Prisma 构建 REST API 和 NestJS + Prisma 构建 REST API 系列教程(二):输入验证 & 类型转换
简介
在本系列的第一章中,你已经创建了一个新的NestJS项目并且集成了Prisma、PostgreSQL 和 Swagger。接下来,你为一个博客程序的服务端构建了一个简陋的 REST API。在第二章中,你学习了如何进行输入验证和类型转换。
在本章中,你将学习如何处理 NestJS 中的错误。你将看到两种不同的策略:
- 第一种,你将学习如何在 API 控制器内的应用程序代码中直接检测和抛出错误。
- 第二种,你将学习如何使用异常过滤器来处理整个应用程序中未处理的异常。
在本教程中,你仍将使用第一章中构建的 REST API。你无需完成第二章即可学习本教程。
开发环境
要学习本教程,你需要:
- 已安装 Node.js。
- 已安装 Docker 或 Docker Compose。如果你使用的是 Linux,请确保你的 Docker 版本是 20.10.0 或更高。你可以通过运行
docker version
命令行来查看你的 Docker 版本。 - 已安装 Prisma VSCode 插件(可选)。Prisma VSCode 插件能为 Prisma 添加一些很棒的智能感知和语法高亮功能。
- 使用 Unix shell(如 Linux 和 macOS 中的终端/shell)以运行本系列中提供的命令。(可选)
如果你没有 Unix shell(例如,你使用的是 Windows 机器),你扔可以继续学习,但命令行需求修改为合适你本机的。
克隆代码库
本教程内容会接着本系列中第一章内容。它包含了一个使用 NestJS 构建的简陋的 REST API。
本教程内容可以在这个 GitHub 仓库的 begin-validation 分支查看。首先,克隆代码库并切换到 end-rest-api-part-1
分支上:
git clone -b end-rest-api-part-1 git@github.com:prisma/blog-backend-rest-api-nestjs-prisma.git
现在,我们开始执行以下操作:
- 导航到克隆的文件夹:
cd blog-backend-rest-api-nestjs-prisma
- 安装依赖:
npm install
- 用 docker 启动 PostgreSQL 数据库:
docker-compose up -d
- 应用数据库迁移:
npx prisma migrate dev
- 启动项目:
npm run start:dev
提示:第四步同时也会生成 Prisma Client 和数据中的种子文件。
现在,你应该可以前往 http://localhost:3000/api/
访问 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 客户端,数据库查询构建器。articles
模块定义了/articles
路由的端点和相关的业务逻辑。
-
prisma
模块有以下内容:schema.prisma
文件定义数据库 schema。migrations
文件夹包含了数据库迁移记录。seed.ts
文件包含一个脚本,该脚本用虚拟数据来初始化你的开发环境数据库。
-
docker-compose.yml
文件定义了 PostgreSQL 数据库 Docker 镜像。 -
.env
文件包含了 PostgreSQL 数据库的数据库连接字符串。
提示:要了解这些组件的更多信息,可以前往本系列教程的第一章。
直接检测和抛出异常
本节将教你如何在程序代码中直接抛出异常。你将解决 GET /articles/:id
端点中的问题。现在,如果你给该端点提供一个不存在的id
值,它并不会报错,而是返回带有 HTTP 200
状态的空值。
例如,尝试制造这样的请求 GET /articles/124235
:
要修复此问题,你必须修改在 articles.controller.ts
中的 findOne
方法。如果该文章不存在,你将抛出一个 NotFoundException
,它是 NestJS 提供的一个内置的异常。
更新 articles.controller.ts
中的 findOne
方法:
// 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 错误。
虽然可以手动在每个控制器中逐个处理异常,但由于多种原因,这不是一个好主意:
- 它会使你的核心程序逻辑因大量错误处理而变得混乱。
- 许多端点都会处理相似的错误,就像找不到资源这种。你就要在很多地方复制相同的错误处理代码。
- 很难更改错误处理逻辑,因为它分散在很多位置。
为了解决这些问题,NestJS 有一个异常层,负责处理整个应用程序中未处理异常的。在 NestJS 中,你可以创建异常过滤器来定义如何处理应用程序中抛出的不同类型的异常。
NestJS 全局异常过滤器
NestJS 中有一个全局异常过滤器,它负责捕获所有未处理异常。要理解这个全局过滤器,让我们看一个例子。使用以下报文向 POST /articles
端点发送两次请求:
{
"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 抛出了一个唯一约束验证错误,那是因为这个 title
字段在 Prisma schema 中被标记为了 @unique
属性。此异常属于 PrismaClientKnownRequestError
并在 Prisma 命名空间级别导出。
由于 PrismaClientKnownRequestError
没有被你的程序直接处理的,所以它才会被内置的全局异常过滤器自动处理。该过滤器生成了一个 HTTP 为 500
的“Interal Server Error”响应。
创建手动异常过滤器
在本节中,你将创建一个自定义异常过滤器来处理你之前看到的 PrismaClientKnownRequestError
。该过滤器将捕获所有类型的 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) {}
}
注意:同时也会创建第二个名为
src/prisma-client-exception.filter.spec.ts
的文件,用以创建测试。你可以忽略此文件。
此时你会收到一个来自 eslint
的错误提示,因为 catch
方法还是空的。更新 PrismaClientExceptionFilter
中的 catch
方法实现,如下所示:
// 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);
}
}
在这里你进行了以下修改:
- 为确保过滤器捕获
PrimsaClientKnownRequestError
类型的异常,你需要把它添加到@Catch
装饰器中。 - 此异常过滤器扩展了 NestJS 核心包中的
BaseExceptionFilter
类。这个类为catch
方法提供了一个默认的实现,即给用户返回一个“服务器内部错误”响应。你可以在此 NestJS 文档中了解更多。 - 你添加了一个
console.error
状态用来把错误信息打印到控制台。这对排障非常有用。
Prisma 会针对许多不同类型的错误抛出 PrismaClientKnownRequestError
。所以你将需要弄清如何从 PrismaClientKnownRequestError
异常中提取错误码。PrismaClientKnownRequestError
异常有一个包含错误码的 code
属性。你可以在 Prisma 错误信息指南中找到这个错误码列表。
你要查找的错误码是 P2002
,它是因为违反了唯一约束而产生的。现在你需要更新 catch
方法,以确保在出现此种错误的时候抛出 HTTP 409 Confict
这个响应。另外你也需要为用户提供一个自定义的错误信息。
像下面这样更新异常过滤器实现:
//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;
+ }
}
}
在这里,你正在访问底层框架的 Response
对象并直接修改响应。默认情况下,NestJS 在底层使用的 HTTP 框架是 express。除了 P2002
之外的任何异常,你仍会发送默认的“内部服务器错误”响应。
注意:在生产环境中,错误消息不要向用户泄漏任何敏感信息。
将异常过滤器应用到程序中
现在,对于 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();
现在,试着向 POST /articles
端点发送一个同样的请求:
{
"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`)"
}
由于 PrismaClientExceptonFilter
是一个全局过滤器,它可以处理应用程序中所有路由上的这种特殊类型错误。
我建议你扩展这个异常过滤器实现,以便可以同样处理其他错误。例如,你可以添加一种条件判定来处理 P2025
错误码,这种错误是由于在数据库中找不到记录而产生的。你应该为此类错误返回 HttpStatus.NOT_FOUNT
状态码。这对 PATCT /articles/:id
和 DELETE /articles/:id
端点非常有用。
彩蛋:使用 nestjs-prisma
包来处理 Prisma 异常
目前为止,你已经学习了在 NestJS 应用中手动处理 Prisma 异常的不同技术。有一个叫作nestjs-prisma
的包,专门用于 Prisma 与 NestJS 一起使用,你也可以使用它来处理 Prisma 异常。这个包是一个非常好的选择,因为它移除了很多模版代码。
在 nestjs-prisma 文档中可以查看这个包的安装和使用说明。当使用此包时,你将不必再手工创建一个单独的 prisma
模块和服务,因为这个包会自动为你创建。
你可以在文档的异常过滤器一节中学习如何使用这个包来处理 Prisma 异常。在本教程的未来章节中,我们也将覆盖 nestjs-prisma
包的更多细节。
总结
恭喜!你在本教程中使用了一个现有的 NestJS 应用程序,并学习了如何集成错误处理。你学习了两种不懂的方式来处理错误:直接在代码中处理和通过创建异常过滤器处理。
在本章中你学习了如何处理 Prisma 错误。但是这些技术本身并不限于 Prisma。你可以使用它们来处理程序中的任何错误。
你可以在 GitHub 代码库的 end-error-handling-part-3
分支找到教程中的完整代码。
【全文完】
原文作者:Tasin Ishmam - Backend web developer
原文地址:www.prisma.io/blog/nestjs…
原文发表于:2022年12月14日
转载自:https://juejin.cn/post/7236182358818406459