所有文章 > API开发 > NestJ框架下的RESTful API简介
NestJ框架下的RESTful API简介

NestJ框架下的RESTful API简介

欢迎阅读 NestJS 的 RESTful API 简介。如果您了解 JavaScript 和 TypeScript,那么按照本文的说明操作将会更加轻松,但精通这些语言并非必需。

NestJS 是构建 Node.js 服务器端应用程序的热门框架之一,备受 Roche、Adidas 和 Autodesk 等公司的信赖,用于打造高效且可扩展的服务器端应用程序。

NestJS 深受 Angular 影响,沿用了 Angular 的模块化设计,包括模块、服务、控制器、管道和装饰器等概念。这些特性使得 NestJS 能够助力开发人员创建可扩展、可测试、松散耦合且易于维护的应用程序。虽然 NestJS 是用 TypeScript 构建的,但它同样支持纯 JavaScript 开发。

NestJS 在 Express.js 或 Fastify 这两个流行的 Node.js 框架之上提供了抽象层。这意味着所有适用于 Express.js 和 Fastify 的优质中间件都可以与 NestJS 兼容。

想要熟悉 NestJS,最佳途径就是动手构建一个具备 CRUD(创建、读取、更新和删除)功能的基础 RESTful API。这正是本文将要引导您完成的。我们将一起构建一个简单的 Blog RESTful API,其中包含处理博客文章 CRUD 操作的终端节点。

开始

代码编辑器

使用 Visual Studio Code 作为代码编辑器可以显著提升 NestJS 的开发效率,因为它提供了智能的 IntelliSense 功能和强大的 TypeScript 支持。

在 Visual Studio Code 中,请转到“文件”>“首选项”>“设置”,然后搜索名为“typescript.preferences.importModuleSpecifier”的用户设置。确保您已将此设置配置为“relative”(相对路径),如下所示。

"typescript.preferences.importModuleSpecifier": "relative"

这将确保 Visual Studio Code 在自动导入模块时使用相对路径,而非绝对路径。选择相对路径导入可以避免代码迁移至不同目录时可能出现的路径问题。

Insomnia 工具

Insomnia 是一款实用的 API 测试工具,我们将借助它来测试即将构建的 NestJS API。

The NestJS CLI

要踏上 NestJS 开发之旅,首先需安装 Nest CLI。Nest CLI 是一个便捷的命令行工具,能够助力我们轻松开发并维护 NestJS 应用程序。它支持在开发模式下运行应用,同时也支持构建和打包生产就绪版本的应用。

npm i -g @nestjs/cli

创建新的 NestJS 项目

现在安装了 Nest CLI,我们可以使用它来创建新项目。

nest new rest-api

此命令会新建一个名为 rest-api 的项目目录,为项目搭建基础架构,并包含以下核心 Nest 文件:

  • app.controller.ts:包含一个基础路由的控制器。
  • app.controller.spec.ts:针对控制器的单元测试文件。
  • app.module.ts:定义了我们应用程序的根模块。
  • app.service.ts:封装了 AppModule 的业务逻辑服务。
  • main.ts:作为我们应用程序的启动入口。

Nest CLI 生成的初始项目结构遵循了将各模块文件保存在各自目录中的通用约定。

测试示例终端节点

NestJS 安装完成后,会附带一个示例 API 端点。我们可以通过向其发送请求来验证其功能。打开 app.controller.ts 文件,你会发现其中有一个使用 @Get() 装饰器创建的 GET 端点。该端点在被访问时会返回字符串 'Hello World!'

@Get()
getHello(): string {
return this.appService.getHello();
}

让我们在项目文件夹中执行 npm run start:dev 命令。这将以监视模式启动我们的 NestJS 应用程序,并实现实时重新加载功能,即当应用程序文件发生更改时,会自动重新加载。

一旦 NestJS 运行起来,我们在 Web 浏览器中打开 http://localhost:3000。这时,应该会看到一个显示 “Hello World!” 问候语的空白页面。

此外,我们还可以使用 API 测试工具(如 Insomnia)向 http://localhost:3000 发送 GET 请求,同样会收到 “Hello World!” 的响应。

接下来,我们将删除这个端点,因为它只是 Nest CLI 添加的用于演示的示例。为此,需要删除 app.controller.tsapp.service.ts 和 app.controller.spec.ts 文件。同时,还要在 AppController 和 app.module.ts 中删除对这些文件的所有引用。

创建特性模块

NestJS 的架构设计鼓励我们按照功能来组织模块。这种基于功能的设计将单个功能相关的代码分组到一个文件夹中,并在一个模块中进行注册。这种设计方式简化了代码库,使得代码拆分变得更加容易。

模块

在 NestJS 中,我们通过使用 @Module 装饰器来创建模块。模块用于注册控制器、服务和任何其他需要导入的子模块。导入的子模块也可以注册自己的控制器和服务。

现在,我们将使用 Nest CLI 为我们的博客文章创建一个模块。

nest generate module posts

执行上述命令后,PostsModule 文件夹中会生成一个空的 posts.module.ts 类文件。

定义接口

接下来,我们将利用 TypeScript 的 interface 来明确博客文章 JSON 对象的结构。

interface 在 TypeScript 中被视为一种虚拟或抽象结构,它仅存在于 TypeScript 层面。interface 主要用于 TypeScript 编译器的类型检查,它不会在 TypeScript 转换为 JavaScript 的过程中生成任何 JavaScript 代码。

现在,我们将借助 Nest CLI 来创建所需的 interface

cd src/posts 
nest generate interface posts

这些命令会在功能相关的文件夹 /src/posts 中为我们的博客文章创建一个 posts.interface.ts 文件。

在使用 TypeScript 的 interface 关键字来定义接口时,请确保在接口声明前加上 export 关键字,这样就可以在整个应用程序中引用和使用该接口了。

export interface PostModel {
id?: number;
date: Date;
title: string;
body: string;
category: string;
}

在命令行提示符下,让我们使用以下命令将当前工作目录重置回项目的根文件夹。

cd ../..

Service

Service 类是用于处理业务逻辑的部分。我们将要创建的 PostsService 将专门负责处理与博客文章管理相关的业务逻辑。

接下来,我们使用 Nest CLI 来为我们的博客文章创建一个服务。

nest generate service posts

执行上述命令后,PostsService 文件夹中会生成一个空的 posts.service.ts 类文件。

@Injectable() 装饰器的作用是将 PostsService 类标记为一个提供者(Provider),这样我们就可以将其注册到 PostsModule 的 providers 数组中,并随后在控制器类中进行注入。关于这部分的详细内容,稍后会进行详细介绍。

Controller

Controller 是负责处理传入的请求并向客户端返回响应的类。一个控制器可以包含多个路由(或称为端点),每个路由都可以实现一组特定的操作。NestJS 的路由机制会根据请求的 URL 将其路由到正确的控制器上。

现在,我们使用 Nest CLI 来为我们的博客文章创建一个控制器。

nest generate controller posts

执行上述命令后,PostsService 相关的文件夹中会生成一个空的 posts.service.ts 类文件。

接下来,我们需要将 PostsService 注入到 PostsController 类的构造函数中。

import { Controller } from '@nestjs/common';
import { PostsService } from './posts.service';

@Controller('posts')
export class PostsController {
constructor(private readonly postsService: PostsService) {}
}

NestJS 利用依赖注入机制,在控制器中自动设置对 PostsService 的引用。这样,我们就可以在控制器类中方便地通过 this.postsService 来调用其提供的方法了。

关于控制器和服务的注册:

我们需要确保 PostsModule 已经正确注册了 PostsController 和 PostsService。值得庆幸的是,之前执行的 nest generate service post 和 nest generate controller post 命令已经自动在 PostsModule 中完成了 PostsService 和 PostsController 类的注册工作。

@Module({
controllers: [PostsController],
providers: [PostsService],
})
export class PostsModule {}

在 NestJS 中,“providers” 这一术语被用来指代 service classes(服务类)、middleware(中间件)、guards(守卫)等。

如果希望 PostsService 类能够被我们应用程序中的其他模块所使用,我们可以在 PostsModule 的 exports 数组中将其导出。这样一来,任何导入了 PostsModule 的模块都能够访问并使用 PostsService

@Module({
controllers: [PostsController],
providers: [PostsService],
exports: [PostsService],
})
export class PostsModule {}

将 PostsModule 导入到 AppModule 中

现在,我们已经拥有了一个内聚且组织良好的模块,它涵盖了与博客文章相关的所有功能。但请注意,除非 AppModule 导入了 PostsModule,否则 PostsModule 中的功能是无法在应用程序中使用的。

如果我们再次查看 AppModule,会发现 PostsModule 已经通过之前执行的 nest generate module post 命令自动添加到了 imports 数组中。这意味着 AppModule 已经成功导入了 PostsModule,从而使得 PostsModule 中的功能可以在整个应用程序中使用。

@Module({
imports: [PostsModule],
controllers: [],
providers: [],
})
export class AppModule {}

添加 Service 和 Controller 逻辑

目前,我们的 PostsService 和 PostsController 都尚未实现任何功能。现在,我们将遵循 RESTful 标准来实现 CRUD(创建、读取、更新、删除)端点及其对应的逻辑。

NestJS 提供了与 MongoDB(通过 @nestjs/mongoose 包)或 PostgreSQL(通过 Prisma 或 TypeORM)等数据库集成和持久化应用数据的便捷方式。但为了演示的简洁性,我们将在 PostsService 中使用一个本地数组来模拟数据库的功能。

import { Injectable } from '@nestjs/common';
import { PostModel } from './posts.interface';

@Injectable()
export class PostsService {
private posts: Array<PostModel> = [];
}

在 NestJS 中,服务(或提供者)的作用域是可以配置的。默认情况下,服务是单例的,这意味着在整个应用程序中只会存在一个服务的实例,并且该实例会被共享。服务的初始化仅在应用程序启动时进行一次。由于服务的默认作用域是单例,因此任何注入 PostsService 的类都将访问到内存中相同的 posts 数组数据。

获取所有帖子

现在,我们来在 PostsService 中添加一个方法,用于返回所有的博客文章。

public findAll(): Array<PostModel> {
return this.posts;
}

接下来,我们将在 PostsController 中添加一个方法,以便将 PostsService 的 findAll() 方法的逻辑暴露给客户端请求。

@Get()
public findAll(): Array<PostModel> {
return this.postsService.findAll();
}

@Get 装饰器被用于创建一个 GET 请求的 /posts 端点。这个端点的路径 /posts 是由定义在控制器上的 @Controller('posts') 装饰器提供的。

获取 1 篇博文

让我们为PostsService添加一个方法,该方法能够返回客户端可能想要查找的特定博客文章。如果在我们维护的帖子列表中未能找到与请求中指定的帖子 ID 相匹配的条目,我们将返回一个 404 NOT FOUND HTTP 错误,以表明所请求的资源未找到。

public findOne(id: number): PostModel {
const post: PostModel = this.posts.find(post => post.id === id);

if (!post) {
throw new NotFoundException('Post not found.');
}

return post;
}

让我们在PostsController中添加一个方法,它将使服务的findAll()方法的逻辑可用于客户端请求。

@Get(':id')
public findOne(@Param('id', ParseIntPipe) id: number): PostModel {
return this.postsService.findOne(id);
}

在这里,@Get 装饰器与参数装饰器 @Param('id') 一起使用,用于创建 GET /post/:id 端点,其中 :id 是代表博客文章唯一标识的动态路由参数。

@Param 装饰器来自 @nestjs/common 包,它能够将路由参数作为方法参数直接提供给我们使用。需要注意的是,@Param 装饰器获取到的值默认是字符串类型。由于我们在 TypeScript 中将 id 定义为数字类型,因此需要进行字符串到数字的转换。NestJS 提供了多种管道(Pipe),允许我们对请求参数进行转换和验证。在这里,我们可以使用 NestJS 的 ParseIntPipe 来将 id 字符串转换为数字类型。

创建帖子

让我们在PostsService中添加一个方法,用于创建一个新的博客文章。这个方法需要为新文章分配一个顺序递增的 id,并返回创建后的文章对象。另外,如果新文章的标题(title)已经与现有文章重复,我们将抛出一个 422 UNPROCESSABLE ENTITY HTTP 错误,表示请求实体无法处理。

public create(post: PostModel): PostModel {
// if the title is already in use by another post
const titleExists: boolean = this.posts.some(
(item) => item.title === post.title,
);
if (titleExists) {
throw new UnprocessableEntityException('Post title already exists.');
}

// find the next id for a new blog post
const maxId: number = Math.max(...this.posts.map((post) => post.id), 0);
const id: number = maxId + 1;

const blogPost: PostModel = {
...post,
id,
};

this.posts.push(blogPost);

return blogPost;
}

让我们在PostsController中添加一个方法,它将使服务的findAll()方法的逻辑可用于客户端请求。

@Post()
public create(@Body() post: PostModel): PostModel {
return this.postsService.create(post);
}

@Post 装饰器被用来创建一个 POST /post 端点。

在 NestJS 中,当我们使用 POSTPUT 和 PATCH 等 HTTP 方法装饰器时,HTTP 请求的主体(Body)通常用于向 API 传输数据,这些数据一般采用 JSON 格式。

为了解析 HTTP 请求的主体,我们可以使用 @Body 装饰器。当使用这个装饰器时,NestJS 会自动对 HTTP 请求的主体执行 JSON.parse() 操作,并将解析后的 JSON 对象作为参数传递给控制器的方法。在这个场景中,我们期望客户端发送的数据符合 Post 类型的结构,因此在 @Body 装饰器中,我们将参数类型声明为 Post

删除帖子

让我们在PostsService中添加一个方法,该方法使用 JavaScript 的 splice() 方法从内存中的帖子数组中移除指定的博客帖子。如果在我们维护的帖子列表中找不到与请求中指定的帖子 ID 相匹配的条目,我们将返回一个 404 NOT FOUND HTTP 错误,表明所请求的资源未找到。

public delete(id: number): void {
const index: number = this.posts.findIndex(post => post.id === id);

// -1 is returned when no findIndex() match is found
if (index === -1) {
throw new NotFoundException('Post not found.');
}

this.posts.splice(index, 1);
}

让我们在PostsController中添加一个方法,它将使服务的findAll()方法的逻辑可用于客户端请求。

@Delete(':id')
public delete(@Param('id', ParseIntPipe) id: number): void {
this.postsService.delete(id);
}

更新帖子

让我们为PostsService添加一个方法,用于查找具有指定 id 的博客文章,并使用新提交的数据对其进行更新。更新完成后,该方法将返回更新后的文章对象。如果在我们的帖子列表中未找到与请求中指定的 id 相匹配的条目,我们将返回一个 404 NOT FOUND HTTP 错误,表明所请求的资源未找到。另外,如果新提交的标题(title)已经被其他博客文章使用,我们将抛出一个 422 UNPROCESSABLE ENTITY HTTP 错误,表示请求实体无法处理。

public update(id: number, post: PostModel): PostModel {
this.logger.log(`Updating post with id: ${id}`);

const index: number = this.posts.findIndex((post) => post.id === id);

// -1 is returned when no findIndex() match is found
if (index === -1) {
throw new NotFoundException('Post not found.');
}

// if the title is already in use by another post
const titleExists: boolean = this.posts.some(
(item) => item.title === post.title && item.id !== id,
);
if (titleExists) {
throw new UnprocessableEntityException('Post title already exists.');
}

const blogPost: PostModel = {
...post,
id,
};

this.posts[index] = blogPost;

return blogPost;
}

让我们在PostsController中添加一个方法,它将使服务的findAll()方法的逻辑可用于客户端请求。

@Put(':id')
public update(@Param('id', ParseIntPipe) id: number, @Body() post: PostModel): PostModel {
return this.postsService.update(id, post);
}

我们使用 @Put 装饰器来处理 HTTP PUT 请求方法。PUT 方法既可以用于创建新资源,也可以用于更新服务器上已有资源的状态。当服务器上的资源已存在且我们知道其位置时,PUT 请求将替换该资源的当前状态。

测试我们的功能模块

首先,使用 npm run start:dev 命令启动我们的开发服务器。然后,打开 Incubator 应用程序,以便测试我们为 PostsModule 创建的 API 端点。

获取所有帖子

向 http://localhost:3000/posts 发送 GET 请求。预期结果是一个表示空数组的响应,并附带 200 OK 成功状态码。

创建帖子

接下来,向 http://localhost:3000/posts 发送 POST 请求,并在请求体中包含以下 JSON 数据:

{
"date": "2021-08-16",
"title": "Intro to NestJS",
"body": "This blog post is about NestJS",
"category": "NestJS"
}

我们应该会收到一个 201 Created 响应代码,表示帖子已成功创建。同时,响应体中还会包含一个 JSON 对象,该对象表示已创建的帖子,并包括一个自动生成的 id 字段。

获取帖子

接下来,向  http://localhost:3000/posts/1 发送 GET 请求。预期结果是一个 200 OK 响应代码,以及包含帖子数据的响应体,其中 id 字段的值为 1。

更新帖子

让我们使用下面的 JSON 数据体向 http://localhost:3000/posts 发送一个 POST 请求。

{
"date": "2021-08-16",
"title": "Intro to TypeScript",
"body": "An intro to TypeScript",
"category": "TypeScript"
}

结果应该是一个 200 OK 响应代码,同时响应体中会包含一个 JSON 对象,该对象表示已更新后的帖子。

删除帖子

接下来,向 http://localhost:3000/posts/1 发送 DELETE 请求。预期结果是一个 200 OK 响应代码,并且响应体中不包含任何 JSON 对象。

日志记录

NestJS 使得在应用程序中添加日志记录变得非常简单。我们应该使用 NestJS 提供的日志记录功能,而不是直接使用 console.log() 语句或原生的 Logger 类。NestJS 的日志记录功能会在终端中为我们提供格式良好、易于阅读的日志消息。

要在我们的 API 中添加日志记录,第一步是在服务类中定义一个 logger 实例。

import { Injectable, Logger, NotFoundException } from '@nestjs/common';
import { PostModel } from './posts.interface';

@Injectable()
export class PostsService {
private posts: Array<PostModel> = [];
private readonly logger = new Logger(PostsService.name);

// ...
}

现在我们已经定义了日志记录器,接下来可以在服务类中添加日志语句。以下是一个日志语句的示例,我们可以将它作为 findAll() 方法的第一行代码添加到 PostsService 类中。

this.logger.log('Returning all posts');

在客户端向我们的 API 发起请求时,每次调用服务方法,这类日志语句都会在终端上输出相应的日志消息,为我们提供便利。

当发送GET /posts请求时,我们应该在终端中看到以下消息。

[PostsService] Returning all posts.

Swagger 

NestJS 使得利用 NestJS Swagger 包将 OpenAPI 规范集成到我们的 API 中变得轻而易举。OpenAPI 规范是一种用于描述 RESTful API 的标准,它主要用于文档化和提供参考信息。

Swagger 设置

让我们为 NestJS 安装 Swagger。

npm install --save @nestjs/swagger swagger-ui-express

Swagger 配置

让我们通过添加Swagger配置来更新引导NestJS应用程序的main.ts文件。

import { NestFactory } from '@nestjs/core';
import { SwaggerModule, DocumentBuilder } from '@nestjs/swagger';
import { AppModule } from './app.module';

async function bootstrap() {
const app = await NestFactory.create(AppModule);

const config = new DocumentBuilder()
.setTitle('Blog API')
.setDescription('Blog API')
.setVersion('1.0')
.build();

const document = SwaggerModule.createDocument(app, config);
SwaggerModule.setup('api', app, document);

await app.listen(3000);
}
bootstrap();

当 NestJS 应用程序正在运行时,我们现在可以访问 http://localhost:3000/api 来查看 API 的 Swagger 文档。请注意,默认情况下,“default”会显示在我们帖子相关路由的上方作为标签。

为了改变这一点,我们可以在 PostsController 类中的 @Controller('posts') 装饰器下方添加 @ApiTags('posts') 装饰器。这将用 “posts” 替换 “default”,以清晰地表明这组端点属于 “posts” 功能或特性集。

@Controller('posts')
@ApiTags('posts')

ApiProperty (api属性)

为了使 PostModel 接口中的属性对 Swagger 可见,我们需要使用 @ApiProperty() 装饰器(对于必填字段)或 @ApiPropertyOptional() 装饰器(对于可选字段)来注释这些字段。但请注意,由于装饰器通常用于类属性,我们需要将接口更改为类,以便能够应用这些装饰器。

因此,我们的下一步是将 posts.interface.ts 文件中的 PostModel 接口转换为类,并在相应的属性上使用 @ApiProperty() 或 @ApiPropertyOptional() 装饰器。

export class PostModel {
@ApiPropertyOptional({ type: Number })
id?: number;
@ApiProperty({ type: String, format: 'date-time' })
date: Date;
@ApiProperty({ type: String })
title: string;
@ApiProperty({ type: String })
body: string;
@ApiProperty({ type: String })
category: string;
}

在 @ApiProperty() 装饰器中,我们为每个字段指定了类型。值得注意的是,id 字段被标记为可选,因为在创建新的博客文章时,我们通常不知道它的 id 会是什么(通常由数据库自动生成)。同时,date 字段被指定为使用 date-time 字符串格式。

这些更改使得 PostModel 的结构能够在 Swagger 中得到正确的记录。当 NestJS 应用程序运行时,我们可以访问 http://localhost:3000/api 来查看 PostModel 的详细文档。

ApiResponse 接口

接下来,我们将利用 Swagger 的 @ApiResponse() 装饰器来全面记录 API 端点可能返回的所有响应类型。这样做有助于我们的 API 用户清晰地了解,通过调用特定的端点,他们可以获得哪些类型的响应。我们将在 PostsController 类中实施这些更改。

对于 findAll 方法,我们将使用 @ApiOkResponse() 装饰器来明确记录 200 OK 成功响应的详细信息。

@Get()
@ApiOkResponse({ description: 'Posts retrieved successfully.'})
public findAll(): Array<PostModel> {
return this.postsService.findAll();
}

对于 findOne 方法,当成功找到对应的帖子时,我们使用 @ApiOkResponse() 装饰器来记录 200 OK 响应。而当未找到帖子时,我们则使用 @ApiNotFoundResponse() 装饰器来记录 404 NOT FOUND HTTP 错误。

@Get(':id')
@ApiOkResponse({ description: 'Post retrieved successfully.'})
@ApiNotFoundResponse({ description: 'Post not found.' })
public findOne(@Param('id', ParseIntPipe) id: number): PostModel {
return this.postsService.findOne(id);
}

对于 create 方法,当成功创建一个新的帖子时,我们将使用 @ApiCreatedResponse() 装饰器来记录 201 CREATED 响应。而当检测到重复的文章标题时,我们会使用 @ApiUnprocessableEntityResponse() 装饰器来记录一个 422 UNPROCESSABLE ENTITY HTTP 错误。

@Post()
@ApiCreatedResponse({ description: 'Post created successfully.' })
@ApiUnprocessableEntityResponse({ description: 'Post title already exists.' })
public create(@Body() post: PostModel): void {
return this.postsService.create(post);
}

对于 delete 方法,如果帖子被成功删除,我们将使用 @ApiOkResponse() 装饰器来记录 200 OK 响应。而当尝试删除一个不存在的帖子时,我们会使用 @ApiNotFoundResponse() 装饰器来记录一个 404 NOT FOUND HTTP 错误。

@Delete(':id')
@ApiOkResponse({ description: 'Post deleted successfully.'})
@ApiNotFoundResponse({ description: 'Post not found.' })
public delete(@Param('id', ParseIntPipe) id: number): void {
return this.postsService.delete(id);
}

对于 update 方法,当帖子被成功更新时,我们将使用 @ApiOkResponse() 装饰器来记录 200 OK 响应。如果尝试更新一个不存在的帖子,我们会使用 @ApiNotFoundResponse() 装饰器来记录一个 404 NOT FOUND HTTP 错误。另外,当发现存在重复的帖子标题时,我们会使用 @ApiUnprocessableEntityResponse() 装饰器来记录一个 422 UNPROCESSABLE ENTITY HTTP 错误。

@Put(':id')
@ApiOkResponse({ description: 'Post updated successfully.'})
@ApiNotFoundResponse({ description: 'Post not found.' })
@ApiUnprocessableEntityResponse({ description: 'Post title already exists.' })
public update(@Param('id', ParseIntPipe) id: number, @Body() post: PostModel): void {
return this.postsService.update(id, post);
}

保存上述更改后,现在您应该能够在 Swagger 网页的 http://localhost:3000/api 地址上查看到每个端点的所有响应代码及其相应的描述信息。

此外,我们可以利用 Swagger 来测试我们的 API,而不仅仅是依赖 Inclusive。只需在 Swagger 网页上点击每个端点下方的“Try it out”按钮,即可轻松验证这些端点是否按预期正常工作。

异常筛选器

异常过滤器为我们提供了对 NestJS 异常处理层的全面掌控。通过它,我们可以为 HTTP 异常响应主体添加自定义字段,或者记录终端上发生的每个 HTTP 异常的日志信息。

接下来,我们需要在 /src/filters 文件夹中创建一个新的文件,命名为 http-exception.filter.ts。然后,在这个文件中,我们将定义一个异常过滤器类。

import { ExceptionFilter, Catch, ArgumentsHost, HttpException, Logger } from '@nestjs/common';
import { Request, Response } from 'express';

@Catch(HttpException)
export class HttpExceptionFilter implements ExceptionFilter {
private readonly logger = new Logger(HttpExceptionFilter.name);

catch(exception: HttpException, host: ArgumentsHost) {
const ctx = host.switchToHttp();
const response = ctx.getResponse<Response>();
const request = ctx.getRequest<Request>();
const statusCode = exception.getStatus();
const message = exception.message || null;

const body = {
statusCode,
message,
timestamp: new Date().toISOString(),
endpoint: request.url,
};

this.logger.warn(`${statusCode} ${message}`);

response
.status(statusCode)
.json(body);
}
}

这个类会利用 NestJS 的记录器功能,在 HTTP 异常产生时向终端输出警告信息。同时,当 HTTP 异常发生时,它还会在响应体中包含两个自定义字段:timestamp 字段用于记录异常发生的时间点,而 endpoint 字段则用于指明是哪个路由触发了该异常。

为了将这个过滤器应用到 PostsController 上,我们需要使用 @UseFilters(HttpExceptionFilter) 装饰器,并传入 HttpExceptionFilter 类的一个新实例。

@Controller('posts')
@UseFilters(new HttpExceptionFilter())
export class PostsController {
constructor(private readonly postsService: PostsService) {}
}

保存这些更改后,NestJS将重新加载我们的应用程序。如果我们使用Inclusion向我们的API发送一个PUT /posts/1请求,它应该会触发一个404 NOT FOUND HTTP错误,因为当它启动时,我们的应用程序中不存在可供更新的博客文章。返回到Incubator的HTTP异常响应主体现在应该包含timestampendpoint字段。

{
"statusCode": 404,
"message": "Post not found.",
"timestamp": "2021-08-23T21:05:29.497Z",
"endpoint": "/posts/1"
}

我们还应该看到下面这行打印到终端。

WARN [HttpExceptionFilter] 404 Post not found.

总结

在本文中,我们深入了解了 NestJS 如何让后端 API 开发变得迅速、简洁且高效。NestJS 提供的应用程序结构助力我们构建出结构清晰、组织有序的项目。

我们涵盖了很多内容,所以让我们回顾一下我们学到的内容:

  • 我们学会了如何使用 NestJS CLI 来快速启动项目。
  • 掌握了在 NestJS 中如何构建功能模块,以提升代码的可维护性和可扩展性。
  • 了解了服务和控制器在 NestJS 中的角色及其使用方法。
  • 学会了利用 Insomnia(或其他 API 测试工具)对 API 进行测试,确保功能的正确性。
  • 掌握了如何在 NestJS 应用程序中添加日志记录,以便更好地追踪和调试问题。
  • 学会了使用 Swagger 来记录和预览 NestJS API,提升了 API 文档的质量和可读性。
  • 掌握了在 NestJS 中如何全面控制 HTTP 异常,提供了更丰富的错误处理机制。

希望你在使用 NestJS 进行开发时能够感受到它的强大与便捷,享受开发的乐趣!

原文链接:https://www.thisdot.co/blog/introduction-to-restful-apis-with-nestjs

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