一文学会最强大的 node.js 后端框架 nest.js

在这里插入图片描述

文章目录

  • nest cli
  • 项目基本结构
  • IOC & DI
    • 基础
    • 注册值
    • 注册时 key 的管理
    • 动态注册类
    • 工厂函数方式注册
    • 设置别名
    • 导出 provider
  • 模块
    • 功能模块
    • 模块的导入导出
    • 模块类中使用注入
    • 全局模块
    • 动态模块
  • 中间件
    • 定义中间件
    • 注册中间件
    • MiddlewareConsumer 类
    • 全局中间件
  • 异常过滤器
    • 抛出异常
    • 自定义异常类
    • 内置的异常类
    • 自定义异常过滤器类
    • ArgumentsHost
    • 应用自定义的过滤器
    • 扩展异常过滤器
  • 管道 pipe
    • 内置管道
    • 使用内置转换管道
    • 自定义管道
    • body 数据验证
      • 基于对象的模式验证 zod
      • 基于类的验证
    • 全局验证管道
    • 参数默认值 `DefaultValuePipe`
  • 守卫 guard
    • 自定义授权守卫
    • 对 nest 执行上下文的理解
      • ArgumentsHost
      • ExecutionContext
    • 使用守卫
    • 基于角色的 controller 鉴权
    • 案例
  • 拦截器 interceptor 切面编程
    • 自定义拦截器
    • 应用拦截器
    • 案例
      • 响应映射
      • 异常映射
      • 流覆盖 —— 缓存拦截器
      • 响应超时拦截
  • 自定义装饰器
    • 给装饰器传递数据
    • 使用管道
    • 组合装饰器

pnpm i -g @nestjs/cli

nest cli

cli 提供 nest 命令,有 6 个子命令:

Usage: nest <command> [options]Options:-v, --version                                   Output the current version.-h, --help                                      Output usage information.Commands:new|n [options] [name]                          Generate Nest application.build [options] [app]                           Build Nest application.start [options] [app]                           Run Nest application.info|i                                          Display Nest project details.add [options] <library>                         Adds support for an external library to your project.generate|g [options] <schematic> [name] [path]  Generate a Nest element.

前 4 个命令不用说。add 命令用于集成一些 nest 适配过的第三方包,generate 命令用生成代码模板。具体有哪些模板查看帮助信息即可。
最好用的要数 resource 模板了,可以直接生成一个 crud。

项目基本结构

src├── app.controller.spec.ts├── app.controller.ts├── app.module.ts├── app.service.ts└── main.ts

以下是这些核心文件的简要概述:

app.controller.ts带有单个路由的基本控制器示例。
app.controller.spec.ts对于基本控制器的单元测试样例
app.module.ts应用程序的根模块。
app.service.ts带有单个方法的基本服务
main.ts应用程序入口文件。它使用 NestFactory 用来创建 Nest 应用实例。
import { NestFactory } from '@nestjs/core';
import { AppModule } from './app.module';async function bootstrap() {const app = await NestFactory.create(AppModule);await app.listen(3000);
}
bootstrap();

要使用底层框架的 API 时,可以给 create 方法设置泛型:NestExpressApplicationNestFastifyApplication

const app = await NestFactory.create<NestExpressApplication>(AppModule);

IOC & DI

基础

ioc 一般有两个步骤:

  1. 标记这个类是可被容器管理的
  2. 在容器注册表中注册这个类
    1. 注册就像是填一张 key-value 的表,key 为唯一标识,value 为要被容器管理的内容,通常也就是要注入的类

应用启动时,会实例化所有注册的类。然后当有其他类依赖这个类的实例时,容器就会拿着 key 去容器中找到该类的实例并注入。

nest 容器管理的实例一般都是单例的。

  • https://angular.cn/guide/dependency-injection

nest 把可以被容器管理的类称为 提供者 provider。和 invertify-util-express 一样使用@Injectable() 来装饰这个类 。

// 标记可注入的类
import { Injectable } from '@nestjs/common';@Injectable()
export class CatsService {}

在根模块中注册类:在模块类的 @Module装饰器中注册。

// 在容器注册表中注册类
import { Module } from "@nestjs/common";
import { CatsService } from './cats/cats.service';@Module({providers: [{provide: CatsService, // key,这里 key 就是类本身,当然最好用 SymboluseClass: CatsService, // value},];
})
export class AppModule {}

当注册的 key 使用类本身时,可简写。另外对于 controller 这种特殊的注入类,用 @controller 声明,注册也有专门的属性。

@Module({controllers: [CatsController], // 注册 controllerproviders: [CatsService] // 简写
})
export class AppModule {}

和 inversify-express-utils 一样使用@inject("key")注入依赖:

import { CatsService } from './cats.service';// 构造注入
class Ikun {constructor(@inject(CatsService) private readonly catsService: CatsService) {}
}// 属性注入
class Ikun {@inject(CatsService)private readonly catsService: CatsService;
}

但其实上面的代码可以省略@inject("key"),因为默认容器会把变量类型当做 key 去查找容器中的对象注入。这里类型就是 CatsService 类,而我们注册时本就是将类作为 key,所以不明确指定 inject key ,也能正确注入。

import { Controller, Get } from '@nestjs/common';
import { CatsService } from './cats.service';
import { Cat } from './interfaces/cat.interface';@Controller('cats')
export class CatsController {// 省略@inject(CatsService)constructor(private readonly catsService: CatsService) {}@Get()async findAll(): Promise<Cat[]> {return this.catsService.findAll();}
}

注册值

前面为了注入类的实例,注册时,都是注册的类。其实注册时,可以直接注册一个值,这样注入的就一定是这个值。比如直接注册一个类的实例,而不是注册类本身。
这个功能在 mock 时会用到。测试时可以明确注入的是哪个对象。

useValue表示注册值。

import { CatsService } from './cats.service';const mockCatsService = {/* mock implementation...*/
};@Module({imports: [CatsModule],providers: [{provide: CatsService,useValue: mockCatsService,},],
})
export class AppModule {}

注册时 key 的管理

前面我们直接用类作为 key。当然我们也可以用字符串代替,为了保证一定唯一,最好用 Symbol 代替。这也是 invertify-express-utils 推荐的。

对于自定义 key 的管理,为了清晰的代码组织,最佳实践是在单独的文件(例如 constants.ts )中定义标记。 对待它们就像对待在其自己的文件中定义并在需要时导入的符号或枚举一样。

动态注册类

有些类也想给容器管理,但是注册时又不能写死,因为想使用同一个 key 注入使用。这时可以动态注册类。比如配置类,开发配置类和生产配置类。

const configServiceProvider = {provide: ConfigService,useClass:process.env.NODE_ENV === 'development'? DevelopmentConfigService: ProductionConfigService,
};@Module({providers: [configServiceProvider],
})
export class AppModule {}

工厂函数方式注册

工厂注册比动态注册更进一步,灵活性拉满。用函数来确定注册的内容。

  • useFactory 属性是一个函数,函数的返回值就是会被注册的内容。
    • 函数里面可能会依赖其他类的对象实例。如果依赖了,这些套娃注入的对象会作为函数的参数传入。
  • inject 属性就和一般的使用注入对象的类一样,接收 key 指明要注入的对象。
    • 这里指明要注入 useFactory 函数的类实例。
    • 只是 useFactory 函数可能不止依赖一个类,所以 inject 接收一个 key 数组。

工厂函数可以是异步的。

const connectionFactory = {provide: 'CONNECTION',useFactory: (optionsProvider: OptionsProvider) => {const options = optionsProvider.get();return new DatabaseConnection(options);},inject: [OptionsProvider],
};@Module({providers: [connectionFactory],
})
export class AppModule {}

设置别名

useExisting 属性允许您为现有的 provider 创建别名。相当于两个 key 都指向同一个注册内容。

感觉这功能可以用来解构,但是这种解耦方式意义不大。真要解耦还得多创建类来解决,名字解构算啥解构。

导出 provider

@Module 装饰器每装饰一次类就是创建了一个容器。
一个容器中要使用另一个容器中管理的对象,那后者容器就要导出才可以。
具体导出内容可以是注册的 key ,也可以是整个注册信息对象,也就是 provider。

使用的模块一方,也要导入要使用的外部模块对象。

const connectionFactory = {provide: 'CONNECTION',useFactory: (optionsProvider: OptionsProvider) => {const options = optionsProvider.get();return new DatabaseConnection(options);},inject: [OptionsProvider],
};@Module({imports: [xxxx], // 导入 providerproviders: [connectionFactory],exports: [connectionFactory], // 导出 connectionFactory provider 整个注册信息对象
})
export class AppModule {}

模块

nest 建议模块化开发。当然,项目小的话,一个根模块走天下也不是不行。

功能模块

nest 建议按功能划分模块,比如用户模块、系统模块。

一个 cats 功能模块的组织结构模板:

src
├──cats
│    ├──dto
│    │   └──create-cat.dto.ts
│    ├──interfaces
│    │     └──cat.interface.ts
│    ├─cats.service.ts
│    ├─cats.controller.ts
│    └──cats.module.ts
├──app.module.ts
└──main.ts

模块的导入导出

新建了一个子模块,怎么在根模块中注册?
只要在根模块 @Module imports 属性中整体导入整个模块即可。
并且导入模块后,就可以直接使用导入模块中 exports 导出的对象了,不用在 imports 中一个一个写出,毕竟整体都被完全导入了。并且模块中共享的实例都是同一个,所以是单例的。

import { Module } from '@nestjs/common';
import { CatsModule } from './cats/cats.module';@Module({imports: [CatsModule],
})export class ApplicationModule {}

模块可以导出他们的内部提供者,还可以再导出自己导入的模块。
模块的组织结构类似一颗树,中间的节点模块,就可能会导入模块又导出模块。

@Module({imports: [CommonModule],exports: [CommonModule],
})
export class CoreModule {}

模块类中使用注入

容器管理的实例也可以注入到模块(类)中(例如,用于配置目的):

  • 注意循环依赖性,模块类不能注入到提供者中。
import { Module } from '@nestjs/common';
import { CatsController } from './cats.controller';
import { CatsService } from './cats.service';@Module({controllers: [CatsController],providers: [CatsService],
})
export class CatsModule {// 注入constructor(private readonly catsService: CatsService) {}
}

全局模块

如果你不得不在任何地方导入相同的模块,那可能很烦人。在 Angular 中,提供者是在全局范围内注册的。一旦定义,他们到处可用。另一方面,Nest 将提供者封装在模块范围内。您无法在其他地方使用模块的提供者而不导入他们。但是有时候,你可能只想提供一组随时可用的东西 - 例如:helper,数据库连接等等。这就是为什么你能够使模块成为全局模块。

import { Module, Global } from '@nestjs/common';
import { CatsController } from './cats.controller';
import { CatsService } from './cats.service';@Global()
@Module({controllers: [CatsController],providers: [CatsService],exports: [CatsService],
})
export class CatsModule {}

@Global 装饰器使模块成为全局作用域。 全局模块应该只注册一次,最好由根或核心模块注册。 在上面的例子中,CatsService 组件将无处不在,而想要使用 CatsService 的模块则不需要在 imports 数组中导入 CatsModule。

动态模块

  • https://nest.nodejs.cn/fundamentals/dynamic-modules

中间件

nest 的中间件是在路由处理程序之前调用的函数。相当于可以对请求做预处理。

nest 的中间件就和 express 中间件一样,有三个参数。

如果当前的中间件函数没有结束请求-响应周期, 它必须调用 next() 将控制传递给下一个中间件函数。否则, 请求将被挂起。

定义中间件

nest 定义中间件有两种,函数式中间件和类中间件。

函数式中间件就和 express 中间件一模一样。

export function logger(req, res, next) {console.log(`Request...`);next();
};

要在类中定义中间件,这个类就要实现 NestMiddleware 接口。并且中间件类也是可以依赖注入的。

import { Injectable, NestMiddleware } from '@nestjs/common';
import { Request, Response, NextFunction } from 'express';@Injectable()
export class LoggerMiddleware implements NestMiddleware {use(req: Request, res: Response, next: NextFunction) {console.log('Request...');next();}
}

注册中间件

中间件不和 provider 一样写在@Module()装饰器里。而是在模块类的configure()方法中使用。并且该模块类需要实现NestModule接口。

在配置中间件时,还可以指定中间件应用的路由和路由的 http 方法。

import { Module, NestModule, RequestMethod, MiddlewareConsumer } from '@nestjs/common';
import { LoggerMiddleware } from './common/middleware/logger.middleware';
import { CatsModule } from './cats/cats.module';@Module({imports: [CatsModule],
})
export class AppModule implements NestModule {configure(consumer: MiddlewareConsumer) {consumer// 可按顺序应用多个中间件.apply(LoggerMiddleware) // 将中间件应用在 GET /cats路由上.forRoutes({ path: 'cats', method: RequestMethod.GET }); }
}

给该路由的所有 http 方法设置中间件。

// 使用 All
.forRoutes({ path: 'cats', method: RequestMethod.All }); // 也可简写
.forRoutes('cats');

路由同样支持模式匹配。例如,星号被用作通配符,将匹配任何字符组合。

forRoutes({ path: 'ab*cd', method: RequestMethod.ALL });

MiddlewareConsumer 类

MiddlewareConsumer 是一个帮助类。它提供了几种内置方法来管理中间件。他们都可以链式调用。

forRoutes() 可接受一个字符串、多个字符串、对象、还可以接受一个 controller 类甚至多个 controller 类。

直接把中间件应用在 controller 上,省的写一堆的路由匹配。

有时我们想从应用中间件中排除某些路由。我们可以使用该exclude() 方法轻松排除某些路由。
此方法可以采用一个字符串,多个字符串或一个 RouteInfo 对象来标识要排除的路由,

consumer.apply(LoggerMiddleware).exclude({ path: 'cats', method: RequestMethod.GET },{ path: 'cats', method: RequestMethod.POST },'cats/(.*)',).forRoutes(CatsController);

全局中间件

如果我们想一次性将中间件绑定到每个注册路由,我们可以在 INestApplication 实例的 use() 方法中注册:

const app = await NestFactory.create(AppModule);
app.use(logger);
await app.listen(3000);

异常过滤器

过滤器的执行时机是处于洋葱模型的末尾。

nest 内置了全局异常过滤器,它会捕获处理整个应用程序中所有抛出的异常。
全局异常过滤器能识别异常类型为 HttpException(及其子类)的异常。其他异常,会直接响应 500 错误。

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

抛出异常

之前我们都是手动定义一个 http 异常工具类,而 nest 内置了这个工具类。

HttpException 构造函数有两个必要的参数来决定响应:

  • response 参数定义 JSON 响应体。它可以是 string 或 object,如下所述。
  • status参数定义HTTP状态代码。
@Get()
async findAll() {throw new HttpException('Forbidden', HttpStatus.FORBIDDEN);
}
{"statusCode": 403,"message": "Forbidden"
}

一般我们设计响应会包含两个属性:

  • statusCode:默认为 status 参数中提供的 HTTP 状态代码
  • message: 基于状态的 HTTP 错误的简短描述

HttpException 构造函数第一个参数可以是对象,自定义错误响应结构。nest 会序列化对象成 json 传输。

@Get()
async findAll() {throw new HttpException({status: HttpStatus.FORBIDDEN,error: 'This is a custom message',}, HttpStatus.FORBIDDEN);
}

自定义异常类

在许多情况下,您无需编写自定义异常,而可以使用内置的 Nest HTTP异常,如下一节所述。
如果确实需要创建自定义的异常,则最好创建自己的异常层次结构,其中自定义异常继承自 HttpException 基类。 使用这种方法,Nest可以识别您的异常,并自动处理错误响应。

export class ForbiddenException extends HttpException {constructor() {super('Forbidden', HttpStatus.FORBIDDEN);}
}
@Get()
async findAll() {throw new ForbiddenException();
}

内置的异常类

为了减少样板代码,Nest 提供了一系列继承自核心异常 HttpException 的可用异常。所有这些都可以在 @nestjs/common包中找到:

  • BadRequestException:错误的请求异常
  • UnauthorizedException:未授权异常
  • NotFoundException:未找到异常
  • ForbiddenException:禁止异常
  • NotAcceptableException:不可接受异常
  • RequestTimeoutException:请求超时异常
  • ConflictException:冲突异常
  • GoneException:已删除异常
  • PayloadTooLargeException:负载过大异常
  • UnsupportedMediaTypeException:不支持的媒体类型异常
  • UnprocessableException:无法处理的异常
  • InternalServerErrorException:内部服务器错误异常
  • NotImplementedException:未实现异常
  • BadGatewayException:错误网关异常
  • ServiceUnavailableException:服务不可用异常
  • GatewayTimeoutException:网关超时异常

所有内置异常也可以使用 options 参数提供错误 cause 和错误描述:

throw new BadRequestException('Something bad happened', { cause: new Error(), description: 'Some error description' 
})
{"message": "Something bad happened","error": "Some error description","statusCode": 400,
}

自定义异常过滤器类

上面都是基于 nest 内置的捕获逻辑在处理 http 异常。如果我们想捕获其他异常进行处理或者想要更改内置的 http 异常处理逻辑。比如将异常写入日志,修改异常的响应结构等操作。则可以自定义过滤器来处理异常。

所有异常过滤器类都需要实现通用的 ExceptionFilter<T> 接口。并使用 catch(exception: T, host: ArgumentsHost)方法处理异常。T 表示异常的类型。

异常过滤器类上可使用 @Catch(异常类型, 异常类型, ...)装饰器,指定要捕获的类型。如果参数为空,则表示捕获所有异常。

让我们创建一个异常过滤器,它负责捕获作为 HttpException 类实例的异常,并为它们设置自定义响应逻辑。为此,我们需要访问底层平台 Request 和 Response。我们将访问 Request 对象,以便提取原始 url 并将其包含在日志信息中。我们将使用 Response.json()方法,使用 Response 对象直接控制发送的响应。
自定义的 http 异常处理:

import { ExceptionFilter, Catch, ArgumentsHost, HttpException } from '@nestjs/common';
import { Request, Response } from 'express';@Catch(HttpException)
export class HttpExceptionFilter implements ExceptionFilter {catch(exception: HttpException, host: ArgumentsHost) {const ctx = host.switchToHttp();const response = ctx.getResponse<Response>();const request = ctx.getRequest<Request>();const status = exception.getStatus();response.status(status).json({statusCode: status,timestamp: new Date().toISOString(),path: request.url,});}
}

ArgumentsHost

ArgumentsHost 对象是一个上下文对象,就和 koa 中的 ctx 一样。ArgumentsHost 是 nest 整个应用的上下文对象,意味着它可以获取 nest 底层框架的一些 API,比如上文就用它获取了 request 和 response 对象。

https://docs.nestjs.cn/8/fundamentals?id=argumentshost%e7%b1%bb

应用自定义的过滤器

使用 @UseFilters装饰器,可以在方法上或类上使用,使用位置不同,作用范围自然就不同,并且可以使用多个,逗号隔开。
该装饰器可以接收过滤器实例,也可以接收过滤器类,建议传递类,可以节约内存。

@Post()
@UseFilters(new HttpExceptionFilter())
async create(@Body() createCatDto: CreateCatDto) {throw new ForbiddenException();
}
@UseFilters(HttpExceptionFilter)
export class CatsController {}

全局范围应用过滤器:

async function bootstrap() {const app = await NestFactory.create(AppModule);app.useGlobalFilters(new HttpExceptionFilter());await app.listen(3000);
}
bootstrap();

扩展异常过滤器

之前是从零自定义异常过滤器,如果希望在已经实现的核心异常过滤器基础上扩展处理逻辑,则可以继承基础异常过滤器 BaseExceptionFilter 并调用继承的 catch() 方法。

import { Catch, ArgumentsHost } from '@nestjs/common';
import { BaseExceptionFilter } from '@nestjs/core';@Catch()
export class AllExceptionsFilter extends BaseExceptionFilter {catch(exception: unknown, host: ArgumentsHost) {super.catch(exception, host);}
}

管道 pipe

管道是实现了 PipeTransform 接口,并被容器管理的类。因此要添加 @Injectable() 装饰器。
整出管道这个概念就是用来做请求参数预处理的,所以管道的执行时机肯定在路由处理函数之前。
Nest 在调用方法之前插入一个管道,管道接收指定给该方法的参数并对它们进行操作。任何转换或验证操作都会在此时发生

对参数的处理一般分两种:转换和验证。
那管道自然也分两种:

  • 转换:管道将输入数据转换为所需的数据输出
  • 验证:对输入数据进行验证。

管道的执行结果是转换或验证成功继续传递; 失败则抛出异常。

内置管道

Nest 自带八个开箱即用的管道,即

  • ValidationPipe: 验证管道

  • DefaultValuePipe: 默认值管道

  • ParseIntPipe: 解析整数管道

  • ParseBoolPipe: 解析布尔值管道

  • ParseArrayPipe: 解析数组管道

  • ParseUUIDPipe: 解析UUID管道

  • ParseEnumPipe: 解析枚举管道

  • ParseFloatPipe: 解析浮点数管道

Parse 开头的管道都有转换与验证参数的能力。以 ParseIntPipe 为例,它确保传给路由处理程序的参数是一个整数(若转换失败,则抛出异常)。

使用内置转换管道

方法参数级别绑定管道

@Get(':id')
async findOne(@Param('id', ParseIntPipe) id: number) {return this.catsService.findOne(id);
}

测试:

GET localhost:3000/abc
{"statusCode": 400,"message": "Validation failed (numeric string is expected)","error": "Bad Request"
}

在上述例子中,我们使用管道用的是类(ParseIntPipe),而不是一个实例,容器会自动依赖注入的。
如果我们想通过传递一些选项来自定义内置管道的行为,那么传递就地实例很有用

@Get(':id')
async findOne(@Param('id', new ParseIntPipe({ errorHttpStatusCode: HttpStatus.NOT_ACCEPTABLE }))id: number,
) {return this.catsService.findOne(id);
}

自定义管道

PipeTransform<T, R> 是一个通用接口,任何管道都必须实现。

  • 泛型接口用 T 表示输入 value 的类型,
  • 用 R 表示 transform() 方法的返回类型。

每个管道都必须实现 transform() 方法,它有两个参数:

  • value 参数是当前处理的方法参数(在被路由处理方法接收之前)
  • metadata 是当前处理的方法参数的元数据。
export interface ArgumentMetadata {type: 'body' | 'query' | 'param' | 'custom';metatype?: Type<unknown>;data?: string;
}
type指示参数是主体 @Body()、查询 @Query()、参数 @Param() 还是自定义参数(了解更多 此处)。
metatype提供参数的元类型,例如 String。注意:如果你在路由处理程序方法签名中省略类型声明或使用普通 JavaScript,则该值为 undefined。
data传递给装饰器的字符串,例如 @Body(‘string’)。如果将装饰器括号留空,则为 undefined。

示例:我们让它简单地接受一个输入值并立即返回相同的值,表现得像一个恒等函数。

import { PipeTransform, Injectable, ArgumentMetadata } from '@nestjs/common';@Injectable()
export class ValidationPipe implements PipeTransform {transform(value: any, metadata: ArgumentMetadata) {return value;}
}

body 数据验证

之前的验证都是请求头上的参数验证。现在来探讨请求体中的验证,比如创建对象接口验证传递的请求体数据。

方案:
我们可以在路由处理程序方法内执行此操作,但这样做并不理想,因为它会破坏单一职责原则 (SRP)。
另一种方法可能是创建一个验证器类并在那里委派任务。这样做的缺点是我们必须记住在每个方法的开头调用此验证器。express 模板中我就是这么干的。

这些方法都不够好。有几种方法可以用干净的 DRY 方式进行对象验证。

“不要重复自己”(Don’t repeat yourself)是软件开发的一个原则,旨在减少可能改变的信息的重复,用不太可能改变的抽象代替它,或者使用数据规范化,首先避免冗余。

基于对象的模式验证 zod

一种常见的方法是使用基于对象的模式验证。

Zod 验证库

zod 库需要在 tsconfig.json 文件中启用 strictNullChecks 配置。

定义一个用 Zod 验证数据对象的管道:

import { PipeTransform, ArgumentMetadata, BadRequestException } from '@nestjs/common';
import { ZodSchema  } from 'zod';export class ZodValidationPipe implements PipeTransform {constructor(private schema: ZodSchema) {}transform(value: unknown, metadata: ArgumentMetadata) {try {const parsedValue = this.schema.parse(value);return parsedValue;} catch (error) {throw new BadRequestException('Validation failed');}}
}

使用 zod 验证管道的流程:

  1. 创建特定的 Zod 验证规则 schema
  2. 创建 ZodValidationPipe 的实例,并在管道的类构造函数中传递上下文特定的 Zod 验证规则 schema
  3. 将管道实例绑定到方法

使用 Zod 验证的 schema 示例:

import { z } from 'zod';export const createCatSchema = z.object({name: z.string(),age: z.number(),breed: z.string(),}).required();export type CreateCatDto = z.infer<typeof createCatSchema>;

@UsePipes() 将管道实例绑定到方法。这是方法级别的验证。

@Post()
@UsePipes(new ZodValidationPipe(createCatSchema)) // schema 传递给 zod 管道实例
async create(@Body() createCatDto: CreateCatDto) {this.catsService.create(createCatDto);
}

基于类的验证

nest 可以搭配 class-transformer 和 class-validator 基于类实现验证。

这种方式不用像 zod 一样额外定义一个验证的模式类,更简洁。

import { IsString, IsInt } from 'class-validator';export class CreateCatDto {@IsString()name: string;@IsInt()age: number;@IsString()breed: string;
}

然后和 zod 一样定义一个通用的验证管道进行验证即可。哈哈,这个通用的验证管道,nest 已经内置了,就是 ValidationPipe。我们直接使用就好了。

  • https://nest.nodejs.cn/techniques/validation

这里以类的方式使用,当然也可以使用实例的方式,那样就可以传递一些配置项了。

@Post()
create(@Body(ValidationPipe) createUserDto: CreateUserDto) {return this.userService.create(createUserDto);
}

ValidationPipe的内部实现类似于此:

import { PipeTransform, Injectable, ArgumentMetadata, BadRequestException } from '@nestjs/common';
import { validate } from 'class-validator';
import { plainToInstance } from 'class-transformer';@Injectable()
export class ValidationPipe implements PipeTransform<any> {async transform(value: any, { metatype }: ArgumentMetadata) {if (!metatype || !this.toValidate(metatype)) {return value;}const object = plainToInstance(metatype, value);const errors = await validate(object);if (errors.length > 0) {throw new BadRequestException('Validation failed');}return value;}private toValidate(metatype: Function): boolean {const types: Function[] = [String, Boolean, Number, Array, Object];return !types.includes(metatype);}
}

全局验证管道

async function bootstrap() {const app = await NestFactory.create(AppModule);app.useGlobalPipes(new ValidationPipe());await app.listen(3000);
}
bootstrap();

参数默认值 DefaultValuePipe

有时候我们希望请求的参数有默认值。比如转换相关的 Parse* 管道需要有值的参数值。他们在收到 null 或 undefined 值就会抛出异常。因此我们希望可以给参数设置默认值。

只需在相关的 Parse* 管道之前的 @Query() 装饰器中实例化一个 DefaultValuePipe,如下所示:

@Get()
async findAll(@Query('activeOnly', new DefaultValuePipe(false), ParseBoolPipe) activeOnly: boolean,@Query('page', new DefaultValuePipe(0), ParseIntPipe) page: number,
) {return this.catsService.findAll({ activeOnly, page });
}

守卫 guard

这也是 nest 整出来的新概念,守卫的设计与异常过滤器、管道和拦截器非常相似,可让你在请求/响应周期的正确位置插入处理逻辑,并以声明方式进行。这有助于使你的代码保持干爽和声明式。

守卫是一个用 @Injectable() 装饰器注释的类,它实现了 CanActivate 接口。

守卫用来鉴权,是作访问控制的中间件,根据运行时存在的某些条件(如权限、角色、ACL 等)确定给定请求是否将由路由处理程序处理。守卫啥,守卫接口。

具体执行时机:
在所有中间件之后、任何拦截器或管道之前执行。

自定义授权守卫

每个守卫都必须实现一个 canActivate() 函数。此函数应返回一个布尔值,指示是否允许当前请求。它可以同步或异步(通过 Promise 或 Observable)返回响应。Nest 使用返回值来控制下一步的动作:

  • 如果它返回 true,请求将被处理。
  • 如果它返回 false,Nest 将拒绝该请求。

canActivate() 函数只有一个参数,即 ExecutionContext 执行上下文类的实例。
ExecutionContext 继承自 ArgumentsHost,并提供了一些获取当前执行过程更多详细信息的扩展方法。

  • 更多介绍:https://nest.nodejs.cn/fundamentals/execution-context

这里我们只是用了在 ArgumentsHost 上定义的获取 request 对象的辅助方法。

import { Injectable, CanActivate, ExecutionContext } from '@nestjs/common';
import { Observable } from 'rxjs';@Injectable()
export class AuthGuard implements CanActivate {canActivate(context: ExecutionContext,): boolean | Promise<boolean> | Observable<boolean> {const request = context.switchToHttp().getRequest();return validateRequest(request);}
}

validateRequest() 为自定义的鉴权函数。函数内的逻辑可以根据需要简单或复杂。这个例子的要点是展示守卫如何适应请求/响应周期。

对 nest 执行上下文的理解

这个上下文就和 koa 中的 ctx 一样。当前请求的执行上下文,包含了很多信息。koa 中间件中拿到了 ctx,就是拿到了一切,可以随意操作该请求。这使得开发很方便。

Nest 也提供了几个实用程序类,帮我们去拿请求的上下文。因为 nest 拿到上下文并不容易,因为它可以有多个执行环境,比如 http 服务,微服务 和 websocket。
nest 提供工具类帮我们屏蔽了底层的差异。

你可能会问,压根不需要屏蔽啊。我什么环境就自己去拿什么上下文不就结了。
这当然是可以的。但是很多时候,我们可能会写一些在各个环境都要使用的代码。比如守卫、过滤器、拦截器。这三个很可能是多环境使用的,当写它们的时候不用关心底层环境,直接统一使用 nest 的工具类拿到上下文信息岂不是爽歪歪。
这个过程就像是 globalThis,不用区分 node 还是 browser。

主要介绍两个这样的类:ArgumentsHost 和 ExecutionContext。

ArgumentsHost

ArgumentsHost 类提供了用于检索传递给处理程序的参数的方法。它允许选择适当的上下文(例如 HTTP、RPC(微服务)或 WebSockets)以从中检索参数。
该框架在你可能想要访问它的地方提供了一个 ArgumentsHost 类的实例,通常作为 host 参数引用。例如,使用 ArgumentsHost 实例调用 异常过滤器 的 **catch()** 方法。

ArgumentsHost 只是作为处理程序参数的抽象。
例如,对于 HTTP 服务器应用(当使用 @nestjs/platform-express 时),host 对象封装了 Express 的 [request, response, next] 数组,其中 request 是请求对象,response 是响应对象,next 是控制应用请求-响应周期的函数。
另一方面,对于 GraphQL 应用,host 对象包含 [root, args, context, info] 数组。

获取某个环境下的处理函数的参数。

比如 express,获取中间件的参数数组。一种方法是使用宿主对象的 getArgs() 方法。也可以使用 getArgByIndex() 方法按索引提取特定参数:

const [req, res, next] = host.getArgs();const request = host.getArgByIndex(0);
const response = host.getArgByIndex(1);

但这种索引获取的方式不推荐用,因为它将应用耦合到特定的执行上下文。

你可以通过使用 host 对象的实用方法之一切换到适合你的应用的应用上下文,从而使你的代码更加健壮和可重用。上下文切换实用程序方法如下所示。

/*** Switch context to RPC.*/
switchToRpc(): RpcArgumentsHost;/*** Switch context to HTTP.*/
switchToHttp(): HttpArgumentsHost;/*** Switch context to WebSockets.*/
switchToWs(): WsArgumentsHost;

切换到 HTTP 上下文环境后,HttpArgumentsHost 对象有两个有用的方法可以用来提取所需的对象。在这种情况下,我们还使用 Express 类型断言来返回原生 Express 类型的对象:

const ctx = host.switchToHttp(); // 切换到 HTTP 环境上下文
const request = ctx.getRequest<Request>();
const response = ctx.getResponse<Response>();

同样,WsArgumentsHost 和 RpcArgumentsHost 具有在微服务和 WebSockets 上下文中返回适当对象的方法。以下是 WsArgumentsHost 的方法:

// websocket 环境下获取环境程序的参数
export interface WsArgumentsHost {getData<T>(): T; // Returns the data object.getClient<T>(): T; // Returns the client object.
}// RPC 环境下获取环境程序的参数
export interface RpcArgumentsHost {getData<T>(): T; // Returns the data object.getContext<T>(): T; // Returns the context object.
}

ExecutionContext

ExecutionContext 扩展 ArgumentsHost,提供有关当前执行过程的更多详细信息。与 ArgumentsHost 一样,Nest 在你可能需要的地方提供了 ExecutionContext 的实例,例如 guard 的 **canActivate()** 方法interceptor 的 **intercept()** 方法
主要扩展了两个方法:

export interface ExecutionContext extends ArgumentsHost {/*** Returns the type of the controller class which the current handler belongs to.*/getClass<T>(): Type<T>;/*** Returns a reference to the handler (method) that will be invoked next in the* request pipeline.*/getHandler(): Function;
}

getHandler() 方法返回对即将被调用的处理程序的引用。getClass() 方法返回此特定处理程序所属的 Controller 类的类型。例如,在 HTTP 上下文中,如果当前处理的请求是 POST 请求,绑定到 CatsController 上的 create() 方法,则 getHandler() 返回对 create() 方法的引用,getClass() 返回 CatsController 类(不是实例)。

const methodKey = ctx.getHandler().name; // "create"
const className = ctx.getClass().name; // "CatsController"

这种能访问当前类和处理程序方法的引用的能力,提供了极大的灵活性。最重要的是,它使我们可以访问通过 Reflector#createDecorator 创建的装饰器或来自守卫或拦截器内的内置 @SetMetadata() 装饰器设置的元数据。

使用守卫

与管道和异常过滤器一样,守卫可以是 controller 范围、方法范围或全局作用域。

让我们构建一个功能更强大的守卫示例,只允许具有特定角色的用户访问。我们将从一个基本的守卫模板开始,并在接下来的部分中构建它。目前,它允许所有请求继续:

import { Injectable, CanActivate, ExecutionContext } from '@nestjs/common';
import { Observable } from 'rxjs';@Injectable()
export class RolesGuard implements CanActivate {canActivate(context: ExecutionContext,): boolean | Promise<boolean> | Observable<boolean> {return true;}
}

下面,我们使用 @UseGuards() 装饰器设置了一个 controller 作用域的守卫。这个装饰器可以接受一个参数,或者一个逗号分隔的参数列表。这使你可以通过一个声明轻松应用一组适当的保护。

@Controller('cats')
@UseGuards(RolesGuard)
export class CatsController {}

为了设置全局守卫,使用 Nest 应用实例的 useGlobalGuards() 方法:

const app = await NestFactory.create(AppModule);
app.useGlobalGuards(new RolesGuard());

基于角色的 controller 鉴权

上面的例子中 RolesGuard 可以鉴权守卫了,但还是不够智能。因为它没有利用到执行上下文,实现更细致的鉴权,它是全都放行。
例如,CatsController 可以针对不同的路由使用不同的权限方案。有些可能只对管理员用户可用,而另一些可能对所有人开放。

我们如何以灵活且可重用的方式将角色与路由匹配?
答:在路由上声明角色。为了灵活可重用,我们希望有这样一门技术,在运行时给这个接口附加一些额外数据。这个技术就是反射。反射可以在运行时添加一些数据,这些数据也称为元数据 。因为反射是底层的原始操作,底层添加的数据,自然也是原始数据,元数据。俗称亚当夏娃。

  • 自定义元数据:https://nest.nodejs.cn/fundamentals/execution-context#reflection-and-metadata

nest 为支持自定义元数据的操作。nest 提供了Reflector#createDecorator 静态方法,该方法可创建装饰器用于自定义元数据。

除了自己创建装饰器, nest 还内置了@SetMetadata()装饰器,开箱即用给路由附加元数据。当然,使用这个没有自己建的装饰器名称更语义化。

  • 了解更多:https://nest.nodejs.cn/fundamentals/execution-context#low-level-approach

例如,让我们使用 Reflector#createDecorator 方法创建一个 @Roles() 装饰器,该方法将元数据附加到处理程序。

import { Reflector } from '@nestjs/core';export const Roles = Reflector.createDecorator<string[]>();

使用:

@Post()
@Roles(['admin'])
async create(@Body() createCatDto: CreateCatDto) {this.catsService.create(createCatDto);
}

上面我们已经给路由设置了元数据,现在就要获取元数据来实现鉴权。
获取元数据依旧依靠反射 Reflector 。

在 Node.js 世界中,通常的做法是将授权的用户信息附加到 request 对象上。比如 password 就是验证 token 后就是把 token 负载添加到 request.user 上。
因此在我们的示例代码中,我们假设 request.user 就包含了用户实例和允许的角色。

import { Injectable, CanActivate, ExecutionContext } from '@nestjs/common';
import { Reflector } from '@nestjs/core';
import { Roles } from './roles.decorator';@Injectable()
export class RolesGuard implements CanActivate {constructor(private reflector: Reflector) {}canActivate(context: ExecutionContext): boolean {const roles = this.reflector.get(Roles, context.getHandler());if (!roles) {return true;}const request = context.switchToHttp().getRequest();const user = request.user;return matchRoles(roles, user.roles);}
}

鉴权失败,默认返回:

{"statusCode": 403,"message": "Forbidden resource","error": "Forbidden"
}

这是因为,当守卫返回 false 时,框架就会抛出 ForbiddenException。

守卫抛出的任何异常都将由 异常层(全局异常过滤器和应用于当前上下文的任何异常过滤器)处理。

因此如果你想返回不同的错误响应,你应该抛出你自己的特定异常。例如:

throw new UnauthorizedException();

案例

JWT 身份验证:

  • https://nest.nodejs.cn/security/authentication

RBAC、声明授权、CASL 真实案例:

  • https://nest.nodejs.cn/security/authorization

拦截器 interceptor 切面编程

拦截器这个执行时机牛皮一点,在请求和响应阶段都能执行。这不就是切面 AOP 的环绕通知(Around Advice)吗。

Nest.js 的拦截器更像是 AOP 中的环绕通知(Around Advice),因为它们可以在方法执行之前和之后执行代码。但是,Nest.js 拦截器没有提供 AOP 的全部功能,比如引入(Introduction)和切点表达式(Pointcut Expression)的概念。

AOP 通常涉及以下概念:

  • 切点(Pointcut):定义何时(即在哪些连接点)执行通知。
  • 通知(Advice):定义切点上要执行的操作(如前置通知、后置通知、环绕通知等)。
  • 切面(Aspect):切点和通知的组合。

拦截器可以:

  • 在函数执行之前/之后绑定额外的逻辑
  • 转换从函数返回的结果
  • 转换从函数抛出的异常
  • 扩展基本函数行为
  • 根据所选条件完全重写函数 (例如, 缓存目的)

自定义拦截器

拦截器是用 @Injectable() 装饰器注释并实现 NestInterceptor 接口的类。

NestInterceptor<T, R> 是一个通用接口,其中 T 表示 Observable(支持响应流)的类型,R 是 Observable 封装的值的类型。

每个拦截器都实现了intercept() 方法,它有两个参数。第一个是 ExecutionContext 上下文实例,前面介绍过。第二个参数是 CallHandler
CallHandler 接口实现了 handle() 方法,你可以使用它在拦截器中的某个点调用路由处理程序方法。如果在 intercept() 方法的实现中不调用 handle() 方法,则根本不会执行路由处理程序方法。

这种方式意味着intercept() 方法有效地封装了请求/响应流。因此,你可以在最终路由处理程序执行之前和之后实现自定义逻辑。

很明显,你可以在 intercept() 方法中编写,在调用 handle() 之前执行的代码,这就是在定义路由处理程序执行之前的逻辑,比如参数验证,权限验证啥的。从这里其实也能看出来,拦截器的用途主要体现在响应之后的处理。因为之前的处理已经有管道、守卫这些了。

但是如何影响路由处理方法调用之后发生的情况呢?一旦调用完 controller 里的方法,响应不就结束了吗?结束了你还怎么影响。

别急,nest 引入了 RxJS,实现了观察者模式。
作为前端,你需要知道 RxJS(响应式编程-流)

「RxJS」是使用 Observables 的响应式编程的库,它基于流,使编写异步或基于回调的代码更容易。
这样一看,我们可能会把「RxJS」来和 Promise 作一些列比较,因为 Promise 也是为了使得编写异步或基于回调的代码更容易而出现的。也确实。
简单列举几点「RxJS」解决了哪些使用 Promise 存在的痛点,如:

  • 控制异步任务的执行顺序。
  • 控制异步任务的开始和暂停。
  • 可以获取异步任务执行的各个阶段的状态。
  • 监听异步任务执行过程中发生的错误,在并发执行的情况下,不会因为一个异步任务出错而影响到其他异步代码的执行。

handle() 方法返回的就是一个 RxJS Observable,我们可以使用强大的 RxJS 操作符来进一步操作响应。这样我们就实现了在路由处理程序执行之后添加自定义逻辑。
使用面向方面的编程术语,路由处理程序的调用(即调用 handle())称为 切入点,表示它是我们附加逻辑的插入点。

举个例子:
例如,传入 POST /cats 请求。此请求发往 CatsController 内部定义的 create() 处理程序。如果调用了一个不调用 handle() 方法的拦截器,则不会执行 create() 方法。另一面,一旦 handle() 被调用(并且其 Observable 已被返回),则 create() 处理程序将被触发。一旦通过 Observable 接收到响应流,就可以对该流执行其他操作,并将最终结果返回给调用者。

示例:
使用拦截器记录用户的请求信息(例如,存储用户调用、异步调度事件或计算时间戳)。
我们在下面展示一个简单的 LoggingInterceptor:

import { Injectable, NestInterceptor, ExecutionContext, CallHandler } from '@nestjs/common';
import { Observable } from 'rxjs';
import { tap } from 'rxjs/operators';@Injectable()
export class LoggingInterceptor implements NestInterceptor {intercept(context: ExecutionContext, next: CallHandler): Observable<any> {console.log('Before...');const now = Date.now();return next.handle().pipe(tap(() => console.log(`After... ${Date.now() - now}ms`)),);}
}

由于handle() 返回一个 RxJS Observable,我们可以使用多种运算符来操纵流。在上面的示例中,我们使用了 tap() 运算符,它在可观察流正常或异常终止时调用我们的匿名日志记录函数,但不会以其他方式干扰响应周期。

应用拦截器

@UseInterceptors() 装饰器应用拦截器。与 pipes 和 guards 一样,拦截器可以是控制器作用域级别、方法作用域级别或全局作用域级别。

@UseInterceptors(LoggingInterceptor)
export class CatsController {}

请注意,我们传递了 LoggingInterceptor 类(而不是实例),将实例化的责任留给框架并启用依赖注入。与管道、守卫和异常过滤器一样,我们也可以传递一个就地实例:

@UseInterceptors(new LoggingInterceptor())
export class CatsController {}

为了设置全局拦截器,我们使用 Nest 应用实例的 useGlobalInterceptors()方法:

const app = await NestFactory.create(AppModule);
app.useGlobalInterceptors(new LoggingInterceptor());

案例

响应映射

我们已经知道 handle() 返回 Observable。该流包含从路由处理程序返回的值,因此我们可以使用 RxJS 的 map()运算符轻松地改变它。

注意:响应映射功能不适用于特定于库的响应策略(禁止直接使用 @Res() 对象)。也就是 controller 中函数不能用原生对象 res 响应数据。

说白了就是拦截器可以实现统一响应处理。

简单演示一下:

import { Injectable, NestInterceptor, ExecutionContext, CallHandler } from '@nestjs/common';
import { Observable } from 'rxjs';
import { map } from 'rxjs/operators';export interface Response<T> {data: T;
}@Injectable()
export class TransformInterceptor<T> implements NestInterceptor<T, Response<T>> {intercept(context: ExecutionContext, next: CallHandler): Observable<Response<T>> {return next.handle().pipe(map(data => ({ data })));}
}

通过上述拦截器,当有人调用 GET /cats 接口时,响应将如下所示(假设路由处理程序返回一个空数组 []):

{"data": []
}

拦截器在为整个应用中出现的需求创建可重用的解决方案方面具有很大的价值。

例如,假设我们需要将每次出现的 null 值转换为空字符串 ‘’。我们可以使用一行代码来完成,并全局绑定拦截器,以便每个注册的处理程序自动使用它。

import { Injectable, NestInterceptor, ExecutionContext, CallHandler } from '@nestjs/common';
import { Observable } from 'rxjs';
import { map } from 'rxjs/operators';@Injectable()
export class ExcludeNullInterceptor implements NestInterceptor {intercept(context: ExecutionContext, next: CallHandler): Observable<any> {return next.handle().pipe(map(value => value === null ? '' : value ));}
}

异常映射

另一个有趣的用例是利用 RxJS 的 catchError() 运算符来覆盖抛出的异常:

import {Injectable,NestInterceptor,ExecutionContext,BadGatewayException,CallHandler,
} from '@nestjs/common';
import { Observable, throwError } from 'rxjs';
import { catchError } from 'rxjs/operators';@Injectable()
export class ErrorsInterceptor implements NestInterceptor {intercept(context: ExecutionContext, next: CallHandler): Observable<any> {return next.handle().pipe(catchError(err => throwError(() => new BadGatewayException())),);}
}

流覆盖 —— 缓存拦截器

完全不执行 controller 中的函数,而是从缓存中读取数据返回。也就是用缓存流覆盖了原本的 controller 请求流。

让我们看一下一个简单的缓存拦截器,它从缓存返回其响应。在一个现实的例子中,我们想要考虑其他因素,如 TTL、缓存失效、缓存大小等,但这超出了本次讨论的作用域。在这里,我们将提供一个演示主要概念的基本示例。

import { Injectable, NestInterceptor, ExecutionContext, CallHandler } from '@nestjs/common';
import { Observable, of } from 'rxjs';@Injectable()
export class CacheInterceptor implements NestInterceptor {intercept(context: ExecutionContext, next: CallHandler): Observable<any> {const isCached = true;if (isCached) {return of([]);}return next.handle();}
}

我们的 CacheInterceptor 有一个硬编码的 isCached 变量和一个硬编码的响应 []。
需要注意的关键点是,我们在这里返回一个由 RxJS of() 运算符创建的新流,因此根本不会调用路由处理程序。当有人调用使用 CacheInterceptor 拦截的接口时,将立即返回响应(硬编码的空数组)。实现了流覆盖。
为了创建通用解决方案,你可以利用 Reflector 并创建自定义装饰器。Reflector 在 guards 章节中有详细描述。

响应超时拦截

使用 RxJS 运算符操纵流的可能性为我们提供了许多功能。让我们考虑另一个常见的用例。想象一下你想要处理路由请求的超时。当你的接口在一段时间后未返回任何内容时,你希望以错误响应终止请求。
以下结构可以实现这一点:

import { Injectable, NestInterceptor, ExecutionContext, CallHandler, RequestTimeoutException } from '@nestjs/common';
import { Observable, throwError, TimeoutError } from 'rxjs';
import { catchError, timeout } from 'rxjs/operators';@Injectable()
export class TimeoutInterceptor implements NestInterceptor {intercept(context: ExecutionContext, next: CallHandler): Observable<any> {return next.handle().pipe(timeout(5000),catchError(err => {if (err instanceof TimeoutError) {return throwError(() => new RequestTimeoutException());}return throwError(() => err);}),);};
};

5 秒后,请求处理将被取消。你还可以在抛出 RequestTimeoutException 之前添加自定义逻辑(例如释放资源)。

自定义装饰器

nest 内置了很多参数装饰器,如 @Session()、@Ip() 等。

  • 参数装饰器列表

在 Node.js 中,很多库经常会将传递的值加到 request 对象的 user 属性上,比如 password 就把 token 解析后的结果放到 req.user 上。

因此在每个路由处理程序中我们就得总是手动提取它们,使用如下代码:

const user = req.user;

这太烦了,因此我们可以创建一个 @User() 参数装饰器来完成这个操作。

import { createParamDecorator, ExecutionContext } from '@nestjs/common';export const User = createParamDecorator((data: unknown, ctx: ExecutionContext) => {const request = ctx.switchToHttp().getRequest();return request.user;},
);

使用:

@Get()
async findOne(@User() user: UserEntity) {console.log(user);
}

给装饰器传递数据

当装饰器的行为取决于某些条件时,可以使用 data 参数将参数传递给装饰器的工厂函数。

比如上面的 user 装饰器获取 token 数据,如果想直接获取 token 负载的对象中的某一个属性的数据,比如 id。那我们就希望装饰器可以接收一个字符串,表示要获取的哪个属性。

import { createParamDecorator, ExecutionContext } from '@nestjs/common';export const User = createParamDecorator((data: string, ctx: ExecutionContext) => {const request = ctx.switchToHttp().getRequest();const user = request.user;return data ? user?.[data] : user;},
);
@Get()
async findOne(@User('id') firstName: string) {console.log(`Hello ${firstName}`);
}

对于 TypeScript 用户,请注意 createParamDecorator() 是泛型。这意味着你可以显式强制执行类型安全,例如 createParamDecorator((data, ctx) => …)。或者,在工厂函数中指定参数类型,例如 createParamDecorator((data: string, ctx) => …)。如果两者都省略,则 data 的类型将为 any。

使用管道

nest 内置的解析参数的装饰器都能应用管道,实现数据验证。我们自定义的装饰器如果也要支持传入管道。
则管道必须使用实例的模式,并且将 validateCustomDecorators 选项设为 true。

@Get()
async findOne(@User(new ValidationPipe({ validateCustomDecorators: true }))user: UserEntity,
) {console.log(user);
}

组合装饰器

nest 支持把多个装饰器组合成一个装饰器,也就是复合装饰器。
比如你想要将所有与身份验证相关的装饰器组合成一个装饰器。这可以通过以下构造来完成:

import { applyDecorators } from '@nestjs/common';export function Auth(...roles: Role[]) {return applyDecorators(SetMetadata('roles', roles),UseGuards(AuthGuard, RolesGuard),ApiBearerAuth(),ApiUnauthorizedResponse({ description: 'Unauthorized' }),);
}

使用,一个顶四个。

@Get('users')
@Auth('admin')
findAllUsers() {}

注意:@nestjs/swagger 包中的 @ApiHideProperty() 装饰器不可组合,并且无法与 applyDecorators 函数一起正常工作。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mzph.cn/diannao/6874.shtml

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

社交媒体数据恢复:batchat

蝙蝠聊天数据恢复方法 1. 数据恢复的基本原理 蝙蝠聊天的聊天记录一旦删除是不能够恢复的。这是因为蝙蝠聊天的聊天记录是保存于本地的&#xff0c;一旦删除&#xff0c;就如同在电脑或手机上删除文件一样&#xff0c;数据不会存储在服务器端。这意味着&#xff0c;如果你删除…

10.k8s的附加组件(coreDNS组件)

目录 一、概念 二、查看k8s集群的coreDNS的IP地址 三、验证 一、概念 service发现是k8s中的一个重要机制&#xff0c;其基本功能为&#xff1a;在集群内通过服务名对服务进行访问&#xff0c;即需要完成从服务名到ClusterIP的解析。k8s主要有两种service发现机制&#xff1a;…

v-show和v-if的区别

首先&#xff0c;在用法上的区别&#xff1a; v-show是不支持template&#xff1b; v-show不可以和v-else-起使用&#xff1b; 其次&#xff0c;本质的区别&#xff1a; v-show元素无论是否需要显示到浏览器上&#xff0c;它的DOM实际都是有渲染的&#xff0c;只是通过CSS的dis…

【AI+音视频总结】如何在几分钟内用智能工具摘取音视频精华?揭秘下一代学习和内容创作神器!

今天无意发现一个网站&#xff0c;可以一步到位完成AI音视频总结。 我之前对于音视频总结的步骤还是借助 工具下载 剪映来完成的。详情可以参考之前写的一篇文章 【AI应用】模仿爆款视频二次创作短视频操作步骤 。 这里介绍的网站是 BibiGPT 。 BibiGPT AI 音视频助理 - 它是…

举个栗子!Minitab 技巧(8):用 PLS 偏最小二乘分析大豆脂肪影响因素

在上一个 &#x1f330; 中&#xff0c;我们用 Minitab 最小二乘法验证了两个变量&#xff08;单位桶数与运输时间&#xff09;之间是否存在某种关系。那么&#xff0c;在更复杂的场景中&#xff0c;如何验证一组预测变量和一个或多个连续响应变量之间的关系&#xff1f; 假设…

岩土工程监测中振弦采集仪的布设方案及实施步骤简析

岩土工程监测中振弦采集仪的布设方案及实施步骤简析 岩土工程监测中&#xff0c;河北稳控科技振弦采集仪是一种常用的地下水位和土层压缩性监测工具。它通过采集振弦的振动信号来确定地下水位和土层的压缩性&#xff0c;为岩土工程的设计、施工和监测提供重要的数据支持。下面…

产品AB测试设计

因为vue2项目升级到vue3经历分享1&#xff0c;vue2项目升级到vue3经历分享2&#xff0c;前端系统升级&#xff0c;界面操作也发生改变&#xff0c;为了将影响降到最低&#xff0c;是不能轻易让所有用户使用新系统的。原系统使用好好的&#xff0c;如果新界面用户不喜欢&#xf…

继承知识及扩展(C++)

1. 继承是什么&#xff1f; 继承是面向对象编程的三大特征之一&#xff0c;也是代码复用的手段之一。之前我们在很多的地方尝试函数的复用&#xff0c;而继承是为了类的复用提供了很好的方式。 &#xff08;1&#xff09;继承的代码怎么写 在一个类后面使用 &#xff1a;继承方…

程序设计——前后端分离实现简单表白墙

文章目录 一、前端页面样式代码二、前后端衔接1. 后端创建 maven 项目2. 针对前后端交互的解释以及后端代码的实现针对 post 请求解释前后端衔接针对 Get 请求解释前后端衔接 3.后端与数据库的联系以及对数据的存取单独封装数据库连接代码解释后端存储 save 数据的代码解释后端…

森林消防泵:守护绿色生命线的无声战士/恒峰智慧科技

在广袤无垠的森林中&#xff0c;生命的绿色如同一块巨大的调色板&#xff0c;为世界增添了无尽的生机与活力。然而&#xff0c;这美丽的画卷也可能因一场突如其来的火灾而瞬间破碎。因此&#xff0c;有一群默默无闻的消防人员&#xff0c;他们配备的是一台台强大的森林消防泵&a…

网络基础「HTTPS」

✨个人主页&#xff1a; 北 海 &#x1f389;所属专栏&#xff1a; Linux学习之旅 &#x1f383;操作环境&#xff1a; CentOS 7.6 腾讯云远程服务器 文章目录 1.基本概念1.1.HTTP协议面临的问题1.2.加密与解密1.3.数字摘要1.4.数字签名 2.解决方案2.1.「对称式加密」2.2.「非对…

MySql#MySql数据库基础

目录 一、什么是数据库 二、主流数据库 三、基本使用 1.连接服务器 2.使用 1.查看你数据库 2.创建数据库 ​编辑 ​编辑 ​编辑​编辑 3.使用数据库 ​编辑 4.创建数据库表 5.表中插入数据 6.服务器&#xff0c;数据库&#xff0c;表之间的关系 四、MySQL架构…

for...in 可以用const声明item

代码&#xff1a; function* foo() {yield 1;yield 2;yield 3;}const genr foo();for (const item of genr) {console.log(item);}for (const i 0; i < 5; i) {console.log("i", i);}在这两段代码中&#xff0c;尽管两者都包含 for 循环&#xff0c;但它们的用途…

Git在无法访问github的访问方法

Git无法下载github上的源代码 代理的情况 问题&#xff1a;Failed to connect to github.com port 443 after 21100 ms: Couldnt connect to server 提示我们需要为Git单独配置代理。 查看我们的代理端口  为git 设置全局代理 git config --global http.proxy 127.0.0.1:&l…

Apache反代理Tomcat项目,分离应用服务器和WEB服务器

项目的原理是使用单独的机器做应用服务器&#xff0c;再用单独的机器做WEB服务器&#xff0c;从网络需要访问我们的应用的话&#xff0c;就会先经过我们的WEB服务器&#xff0c;再到达应用程序&#xff0c;这样子的好处是我们可以保护应用程序的机器位置&#xff0c;同时还可以…

LNMP一键安装包

LNMP一键安装包是什么? LNMP一键安装包是一个用Linux Shell编写的可以为CentOS/RHEL/Fedora/Debian/Ubuntu/Raspbian/Deepin/Alibaba/Amazon/Mint/Oracle/Rocky/Alma/Kali/UOS/银河麒麟/openEuler/Anolis OS Linux VPS或独立主机安装LNMP(Nginx/MySQL/PHP)、LNMPA(Nginx/MySQ…

【学一点儿前端】Bad value with message: unexpected token `.`. 问题及解决方法

问题 今天从vue3的项目copy一段代码到vue2项目&#xff0c;编译后访问页面报错了 Bad value with message: unexpected token ..注意到错误字符‘.’&#xff0c;这个错误通常发生在处理 JavaScript 或者 HTML 中的动态表达式中&#xff0c;日常使用二分法不断缩小报错代码范…

2024“天一永安杯“宁波第七届网络安全大赛极安云科战队部分WP

“天一永安杯”2024 宁波第七届网络安全大赛暨第九届大学生网络技术与信息安全大赛 大赛竞赛形式 一、线上初赛 参赛人员&#xff1a;各单位自行选拔3人&#xff08;设队长1名&#xff09;组成团队&#xff0c;不足3人不允许参赛。 竞赛时间&#xff1a;8&#xff1a;30-12&…

LLMs:《Better Faster Large Language Models via Multi-token Prediction》翻译与解读

LLMs&#xff1a;《Better & Faster Large Language Models via Multi-token Prediction》翻译与解读 目录 《Better & Faster Large Language Models via Multi-token Prediction》翻译与解读 Abstract 2、Method方法 Memory-efficient implementation 高效内存实…

详解LLMOps,将DevOps用于大语言模型开发

大家好&#xff0c;在机器学习领域&#xff0c;随着技术的不断发展&#xff0c;将大型语言模型&#xff08;LLMs&#xff09;集成到商业产品中已成为一种趋势&#xff0c;同时也带来了许多挑战。为了有效应对这些挑战&#xff0c;数据科学家们转向了一种新型的DevOps实践LLM-OP…