Nestjs🐺⚡ | Nodejs 框架(上)| 控制器、异常过滤器、提供程序

2025-05-25

Nestjs🐺⚡ | Nodejs 框架(上)| 控制器、异常过滤器、提供程序

Nestjs 是一个服务器端框架,经常与术语“服务器端 Angular”混淆

尽管 Nest 遵循了 Google Angular 的模式和设计原则,但它的设计与 Angular 有很大不同

Nestjs 是传统 Node.js 服务器端工具和包的抽象层

因此请不要将其与 http 服务器(例如:express、koa、fastify、hapi 等)进行比较。Nestjs 实际上使用 express 和 fastify 作为其 http 服务器平台

Nestjs 集中了所有必要的技术/工具,以便使用 Nodejs 构建完美、可靠且持久的企业服务器。它与 Django、Spring Boot、Ruby on Rails 等服务器端框架并驾齐驱。

它遵循微服务架构,但也可以用于单片服务器

Nestjs 的特点:(来源:https://docs.nestjs.com

  • 可扩展、可靠、多功能、先进的框架
  • 提供干净、直接且易于理解的架构
  • 开箱即用:
    • 依赖注入,
    • 使用控制器的装饰器进行路由
    • 安全helmet,,corsnode-csrf
    • 异常过滤器(未处理的异常层)
    • 卫兵
    • 使用提供程序将逻辑与控制器分离
    • 强大的模块系统
    • 生命周期事件等等
    • 单元测试和集成测试jest支持super-test
  • 它提供/支持(通过包):
    • http 服务器 (express/fastify)
    • GraphQL 服务器
    • websocket 服务器 (socket.io/ws)
    • 数据库 orm(sequelize/mongoose/typeorm/knex/prism)
    • 使用以下方式验证请求主体class-validator
    • 缓存使用cache-manager
    • 任务调度使用cron
    • 任务队列使用bull&更多其他工具

这不是一个完整的教程。如果你想完整学习 Nestjs,请访问https://docs.nestjs.com。它包含有关 Nestjs 的完整详细信息。

这些名称和技术术语可能让人眼花缭乱,但其实实现起来相当简单。有些只需要 5-10 行代码就能实现。但对于企业服务器或用户群较小的服务器来说,它们都同样重要。Nestjs 为我们涵盖了架构和依赖项。

事实上,Nestjs 实际上可以帮助并指导我们作为新的后端开发人员使用所有重要的工具,并作为工具使用

Nestjs 也有一个强大的命令行工具,名为@nestjs/cli。它可以帮助操作文件/模块。它有点像 Angluar 的命令行工具,但只处理文件和模块。它可以帮助你更有效地组织项目。

我将完成 Nestjs 教程的三部分。在第一部分或本部分中,我将仅介绍ControllersExceptionFilters&Providers

但首先让我们使用以下命令创建项目:

$ npm install -g @nestjs/cli
$ nest new hello-world && cd hello-world
$ npm run start:dev
Enter fullscreen mode Exit fullscreen mode

这将创建以下目录结构:

hello-world/
├── src/
│   ├── app.controller.ts
│   ├── app.controller.spec.ts
│   ├── app.module.ts
│   ├── app.service.ts
│   ├── app.service.spec.ts
│   └── main.ts
├── test/
│   ├── app.e2e-spec.ts
│   └── jest.e2e.json
├── .gitignore
├── .eslintrc.js
├── .prettierrc
├── README.md
├── nest-cli.json
├── package.json
├── package-lock.json
├── tsconfig.build.json
└── tsconfig.json
Enter fullscreen mode Exit fullscreen mode

hello现在让我们在里面创建一个目录,src并在里面hello为本教程创建 4 个文件

  • hello.controller.ts
  • hello.module.ts
  • hello.service.ts
  • hello-body.dto.ts

1. 控制器

控制器是 Nest 的构建块。控制器用于处理传入的请求。您可以使用 http 方法修饰符(例如 Get、Post、Put、Delete 等)和装饰器来定义路由路径。

控制器示例:

// hello.controller.ts

import {Controller, Logger, Get, NotFoundException, Param} from "@nestjs/common"

@Controller()
export class HelloController{
        /* a logger from nestjs for logging error/other info */
    logger: Logger = new Logger(HelloController.name)
    db: {id: string, message: string}[] = []; // temporary database

    @Get("hello")
    async replyHello() {
        try {
            return "Hello";
        } catch (error) {
            this.logger.error(error?.message ?? "");
            throw error;
        }
    }

    @Get("hello/:helloId") // dyanmic parameter just like express, koa-router etc...
        async replyExactHello(
           /*pass the same dynamic parameter from "hello/:helloId" in 
             @Param decorator's first to let nestjs find the parameter
             correctly
            */
           @Param("helloId") id: string
        ) {
        try {
            /*returning the correct temp hello message*/
            const message = this.db.find(hello=>hello.id===id)?.message
            if(!message) throw new NotFoundException("desired `hello` not found") //404 error
            return message;
        } catch (error) {
            /* will log the error & autmatically send the error as response with all required data */
            this.logger.error(error?.message ?? "");
            throw error;
        }
    }
}
Enter fullscreen mode Exit fullscreen mode

Nestjs 使用装饰器模式,主要用Typescript编写,也支持 JavaScript。你还可以使用以下命令对请求体进行验证:class-validator

////// main.ts //////
import { NestFactory } from "@nestjs/core";
import { AppModule } from "./hello.module";
import { ValidationPipe } from "@nestjs/common";

async function bootstrap() {
    /* Creates a nest application for monolithic servers */
    const app = await NestFactory.create(AppModule, { logger: false });

    // validation is done through Nestjs pipes. 
    // Nestjs Pipes run before a specified request (kinda like middlewre)
    /* Enabling Validation pipe globally.
       Thoough it can be done in module level too
        */
    app.useGlobalPipes(new ValidationPipe());
    await app.listen(PORT);
}
bootstrap()

Enter fullscreen mode Exit fullscreen mode

这里我只是创建了一个 Nestjs 服务器实例并ValidatonPipe全局添加。管道只是在控制器之前运行的方法。管道也可以通过装饰器在方法/参数级别使用。你甚至可以创建自己的自定义管道。你可能已经注意到了。这是 Nestjs 使所有功能正常运行的关键。你可以在“异常过滤器”部分之后找到更多关于它的@UsePipes信息AppModuleAppModule

class-validator现在让我们用& 装饰器创建一个主体验证模式

////// hello-body.dto.ts //////
import {IsDefined, IsNotEmpty} from "class-validator"

export class HelloBodyDTO{
  @IsDefined()
  @IsNotEmpty({message: "A custom error message if you want to"})
    message!: string;
}
Enter fullscreen mode Exit fullscreen mode

@IsDefined&@IsNotEmpty将验证定义为 & 的字符串至少长度为 1,换句话说,该字符串不应该只是""。现在让我们在请求控制器中使用它@Post

////// hello.controller.ts //////
import {Controller, Logger, Get, NotFoundException, Post, Body} from "@nestjs/common"
import {HelloBodyDTO} from "./hello-body.dto"
import {v4 as uuid} from "uuid"

@Controller()
export class HelloController{
  // ... previously written stuff from the `Controller` part

  // decorator name is similar to http verbs e.g. POST -> @Post
  @Post("hello")
    saveHello(
        /*Just pass the class as a type & the validation will be done automatically*/
        @Body() body: HelloBodyDTO
    ){
        try{
      const id = uuid()
            const hello = {id, message: body.message}
            this.db.push(hello) // saving in the temp db
            return hello;
        }
        catch (error){
                this.logger.error(error?.message ?? "");
        throw error;
        }
    }
}

Enter fullscreen mode Exit fullscreen mode

2. 异常过滤器

异常过滤器是控制器抛出错误时运行的错误处理程序。它会自动处理该错误并发送适当的、用户友好的错误响应。

如果您仔细阅读了代码,您可能会发现 Nestjs 默认HttpExceptionFilter(全局)使用了 。该包@nestjs/common提供了许多HttpException继承的异常,例如NotFoundExceptionBadRequestException等等。您甚至可以创建自己的自定义异常。NotAcceptableExceptionUnauthorizedExceptionExceptionFilter

了解如何创建自定义 ExceptionFilter

如果要在路由处理程序中使用自定义异常过滤器,则必须使用@UseFilter装饰器

// ... other stuff
import {ForbiddenException} from "@nestjs/common"
import {CustomHttpExceptionFilter} from "./custom-filter"

// a simple handler method inside a Controller
@Post("throw-error")
@UseFilters(new CustomHttpExceptionFilter())
errorThrowingHandler(){
    throw new ForbiddenException("its forbidden");
}
// ... other stuff
Enter fullscreen mode Exit fullscreen mode

在每个处理程序/控制器之前使用这段长代码@UseFilters(new HttpExceptionFilter())并不难,但如果您的应用程序有一个全局使用它的用例,那么您只需使用useGlobalFiltersNestjs 服务器实例并将所有全局过滤器作为参数传递

///// main.ts /////

// ...other stuff
import {CustomHttpExceptionFilter} from "./custom-filter"

app.useGlobalFilters(new CustomHttpExceptionFilter())
// ...other stuff
Enter fullscreen mode Exit fullscreen mode

3. 提供商

提供程序是 Nestjs 的另一个重要组成部分。到目前为止,我一直使用临时变量来存储数据。这就是为什么可以通过控制器处理程序来完成这么简单的事情。但对于更大、更复杂的逻辑,代码分离和重用会很困难。这时,提供程序就派上用场了……

@Injectable你可以在类顶部使用装饰器声明/创建一个提供程序。然后可以通过提供程序进行依赖注入/日志记录等。

以下是一个提供程序示例。为了便于理解,我使用自定义变量作为数据库。但大多数情况下createfindfindByIddeletedeleteById等方法都是由数据库 ORM 提供的。因此,在实际场景中,这些方法无需在提供程序中实现。提供程序应该用于处理更复杂的逻辑。但为了演示,我们先将这些方法视为复杂的逻辑。

////// hello.service.ts ///////

import { Injectable } from "@nestjs/common"
import {v4 as uuid} from "uuid"

@Injectable()
export class HelloService{
  db: {id: string, message: string}[] = []

  async findById(id: string){
        return this.db.find(hello=>hello.id===id)
  }

  async create(message: string){
        const id = uuid()
        const hello = {id, message}
        this.db.push(hello)
        return hello;
  }

  async deleteById(id: string){
        this.db = this.db.filter(hello=>hello.id!==id)
    return `DELETED node ${id} from db`
  }
}
Enter fullscreen mode Exit fullscreen mode

现在,让我们通过依赖注入来转换HelloController使用HelloService。但在我们将HelloService其放入之前HelloModule

模块是一个带有@Module()装饰器注解的类。@Module()装饰器提供 Nest 用于组织应用程序结构的元数据。每个应用程序至少有一个模块,即根模块。根模块是 Nest 构建应用程序图的起点——Nest 使用根模块来解析模块和提供商之间的关系以及依赖关系的内部数据结构。

该模块是帮助 Nest 构建依赖注入依赖关系图的主要部分。示例如下app.module.ts

////// app.module.ts //////
import { Module } from '@nestjs/common';
/*This is the base '/' controller */
import { AppController } from './app.controller';
/* basic provider for AppController */
import { AppService } from './app.service';

@Module({
  /*this where descendent modules get added
            we've to do this if we were importing another inside
            an other module to be able to use its providers
     */
  imports: [],
  controllers: [AppController],
  providers: [AppService],
})
export class AppModule {}
Enter fullscreen mode Exit fullscreen mode

我们必须将Injectable我们在模块中的控制器/提供程序内部使用的每个提供程序()/控制器添加到模块中。让我们将HelloService&HelloController放入HelloModule

////// hello.module.ts //////
import {Module} from "@nestjs/common"
import {HelloService} from "./hello.service"
import {HelloController} from "./hello.controller"

@Module({
  /* put all providers that is under this module graph to help Nest to
         inject those in the controllers
  */
    providers: [HelloService],
  /* put controllers here for letting Nest recognize all the route/path &
     their handlers
    */
  controllers: [HelloController],
  /*put those providers which you wanna use outside of this module
    In an outside module when HelloModule gets imported
  */
  exports: []
})
export class HelloModule{}
Enter fullscreen mode Exit fullscreen mode

为了让 Nest 识别HelloModule为模块,让我们在数组HelloModule中添加importsAppModule

///// app.module.ts //////
// ... previously imported stuff
import {HelloModule} from "./hello/hello.module"

@Module({
    imports: [HelloModule],
    // ... other properties previously added
})
export class AppModule {}
Enter fullscreen mode Exit fullscreen mode

AppModule被用作NestFactory实例的入口点main.ts

现在我们可以轻松地在HelloService内部HelloController或其他模块的控制器/提供程序中使用

////// hello.controller.ts //////
// ... previously imported stuff
import {HelloService} from "./hello.service"

@Controller()
export class HelloController{

    logger: Logger = new Logger(HelloController.name)

    /* just create a contructor arg and set the type as the provider
             & that's gonna do the work
         */
    constructor(private readonly helloService: HelloService){}

    @Get("hello")
    async replyHello() {
        try {
            return "Hello";
        } catch (error) {
            this.logger.error(error?.message ?? "");
            throw error;
        }
    }

    @Get("hello/:helloId")
        async replyExactHello(@Param("helloId") id: string) {
        try {
            /*using provider's methods*/
            const message = await this.helloService.find(id)?.message;
                        if(!message) throw new NotFoundException("desired `hello` not found") //404 error
            return message;
        } catch (error) {
            this.logger.error(error?.message ?? "");
            throw error;
        }
    }


      @Post("hello")
        saveHello(@Body() body: HelloBodyDTO){
            try{
        /* creating `hello` using the provider HelloService */
                return await this.helloService.create(body.message)
            }
            catch (error){
                this.logger.error(error?.message ?? "");
        throw error;
            }
        }
}
Enter fullscreen mode Exit fullscreen mode

不要害怕 Nestjs 的模块系统。一开始可能会有点难,但一旦你理解了,一切都会变得有意义。而且 Nestjs 的所有酷炫的依赖注入功能都需要这个模块系统。

顺便说一句,您无需在模块中手动添加提供程序/控制器。如果您使用nest-cli 创建模块/提供程序/控制器,它将自动完成。上述模块管理步骤只需使用以下三个命令即可自动完成

创建一个模块

$ nest g module hello
Enter fullscreen mode Exit fullscreen mode

创建控制器

$ nest g controller hello
Enter fullscreen mode Exit fullscreen mode

创建提供者

$ nest g provider hello
Enter fullscreen mode Exit fullscreen mode

别伤害我😶。我知道,我应该早点展示这个更简单的方法😁。但是 Nestjs 中模块工作原理的这种想法常常困扰着人们,让他们不敢使用 Nestjs。所以深入了解它很重要。你可以在这里深入了解 Nestjs 模块系统。

这是完整的应用程序

文章来源:https://dev.to/krtirtho/nestjs-the-framework-of-nodejs-part-1-gl7
PREV
Reactjs | 完美用例的完美工具🛠️💥
NEXT
2021 年 JavaScript/Typescript 技巧汇编🚀