Loading...
nestjs文档
1/8/2026, 11:16:03 AM
开发
同Spring-boot一样, Controller 路由层、Service服务层(核心)、Repository(数据库层)。
nestjs 按模块切分服务,每个模块可以有自己的 路由-Controller、服务-Service、数据库-Repository 等。类似webman或thinkPHP的多应用模式中的每一个应用,每个模块包括(以article文章模块为案例):
以下文件的定义可选:
类似vue的组件注册,在模内块定义的文件,需要在模块内注册,子模块需要在根模块中注册:
@Controller装饰器@Injectable装饰器@Module装饰器中的providers属性和controllers属性app.module.ts中注册(或者作为子模块注册到父模块中),使用@Module装饰器的imports属性 (当然Controller和Service也可以直接在app.module.ts中直接注册,绕过子模块定义文件)每个模块自动成为共享模块 。NestJS 的设计鼓励 “模块自治”。
一旦创建,它就可以被任何模块重复使用。假设我们想在多个其他模块之间共享 CatsService 的实例。为此,我们首先需要通过将该提供者添加到模块的 exports 数组来导出 CatsService,如下所示:
import { Module } from '@nestjs/common'; import { CatsController } from './cats.controller'; import { CatsService } from './cats.service'; @Module({ controllers: [CatsController], providers: [CatsService], exports: [CatsService] }) export class CatsModule {}
现在任何导入 CatsModule 的模块都可以访问 CatsService,并且将与所有其他导入该模块的模块共享同一个实例()。 在 Nest 中,模块默认是单例的(每个模块独立注册,但多个模块多次重复注册同一Service会创建多个实例),因此您可以轻松地在多个模块之间共享同一个提供者实例。
模块可以导出其内部提供者。此外,它们还能重新导出所导入的模块。
@Module({ imports: [CommonModule], exports: [CommonModule], }) export class CoreModule {}
当需要提供一组开箱即用的全局提供者(如辅助工具、数据库连接等)时,可使用 @Global() 装饰器将模块标记为全局模块。
@Global() 装饰器使模块具有全局作用域。全局模块通常应由根模块或核心模块仅注册一次。在下面例子中,CatsService 提供者将无处不在,希望注入该服务的模块无需在其 imports 数组中导入 CatsModule。
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 {} // 注意1:全局模块的导出是单例的,因此在多个模块中注入相同的全局模块将返回相同的实例。 // 注意2:CatsModule还需要再根模块哪里注册一下,否则无法使用
动态模块允许创建可在运行时配置的模块。当需要提供灵活、可定制的模块(其提供者能根据特定选项或配置创建时)特别有用。下面是数据库连接模块的示例:
import { Module, DynamicModule } from '@nestjs/common'; import { createDatabaseProviders } from './database.providers'; import { Connection } from './connection.provider'; @Module({ providers: [Connection], exports: [Connection], }) export class DatabaseModule { static forRoot(entities = [], options?): DynamicModule { const providers = createDatabaseProviders(options, entities); return { module: DatabaseModule, providers: providers, exports: providers, }; } }
该模块默认定义了 Connection 提供者(在 @Module() 装饰器元数据中),此外根据传入 forRoot() 方法的 entities 和 options 对象,还会暴露一系列提供者,例如存储库。请注意动态模块返回的属性会扩展 (而非覆盖)@Module() 装饰器中定义的基础模块元数据。这样既保留了静态声明的 Connection 提供者又能导出动态生成的存储库提供者。
若需在全局范围注册动态模块,请将 global 属性设为 true。
{ global: true, module: DatabaseModule, providers: providers, exports: providers, }
可按以下方式导入并配置 DatabaseModule:
import { Module } from '@nestjs/common'; import { DatabaseModule } from './database/database.module'; import { User } from './users/entities/user.entity'; @Module({ imports: [DatabaseModule.forRoot([User])], exports: [DatabaseModule], // 重新导出动态模块【可选】 }) export class AppModule {}
快速生成CRUD接口: 要快速创建带有内置验证功能的 CRUD 控制器,可以使用 CLI 的 CRUD 生成器:(自动生成controller、service、dto、module)
nest g resource [name] nest g resource modules/user --no-spec --type rest # 设置生成相对目录,不要测试案例,生成rest API风格(nest g中的g是generate的缩写)
生成controller:
使用 CLI 创建控制器,只需执行 nest g controller [name] 命令
nest g controller [name]
生成service:
要使用 CLI 创建服务,只需执行 nest g service [name] 命令。
nest g service [name]
NestJS 将中间件、拦截器、守卫、管道等拆分为不同概念,看似增加了复杂度,实则是为了解决 Express 原本中间件功能过于笼统导致的代码混乱问题。这种拆分遵循了「单一职责原则」,让不同类型的逻辑各司其职,最终降低大型应用的维护成本。
Express 的中间件是「万能工具」,可以处理路由匹配、参数验证、权限校验、异常捕获等所有逻辑,但在复杂应用中会暴露明显问题:
NestJS 拆分的核心逻辑:按「执行阶段」和「职责」划分 NestJS 在 Express/Fastify 等底层框架之上,抽象出不同类型的组件,每个组件只负责特定阶段的特定任务,形成清晰的 「请求生命周期」: | 组件类型 | 执行阶段 | 核心职责 | 与 Express 中间件的区别 | | -------------- | -------------------------------------- | ------------------------------------------ | ----------------------------------------------- | | 中间件 | 请求进入最早期(路由匹配前) | 基础请求处理(如跨域、日志、静态资源) | 功能与 Express 中间件一致,保留底层灵活性 | | 守卫 | 路由匹配后,控制器执行前 | 权限校验、访问控制(如登录验证、角色判断) | 专注于「是否允许请求继续执行」,可结合依赖注入 | | 管道(转换器) | 控制器执行前(参数解析阶段) | 数据验证、类型转换(如参数校验、格式转换) | 专门处理输入数据,失败时直接阻断请求 | | 拦截器(AOP) | 控制器执行前后(环绕阶段,控制器前后) | 增强逻辑(如日志记录、缓存、异常转换) | 可修改请求 / 响应数据,支持异步操作和返回值处理 | | (异常)过滤器 | 异常抛出时 集中异常处理 | (如格式化错误响应) | 统一捕获所有阶段的异常,避免重复处理 |
其中 对于小型项目:
class-validator类校验 就够用了所以重点是守卫就可以, 此外有一点要注意:
除了异常过滤器使用
@catch装饰器,其他的 中间件、守卫、管道和拦截器 都是用@Injectable()装饰器。
依赖注入是一种控制反转(IoC) 技术,它将依赖项的实例化委托给 IoC 容器(在我们这里是 NestJS 运行时系统),而不是在代码中直接硬编码创建。 我们定义一个提供者。
@Injectable() 装饰器将 CatsService 类标记为一个提供者。import { Injectable } from '@nestjs/common'; import { Cat } from './interfaces/cat.interface'; @Injectable() export class CatsService { private readonly cats: Cat[] = []; findAll(): Cat[] { return this.cats; } }
import { Module } from '@nestjs/common'; import { CatsController } from './cats/cats.controller'; import { CatsService } from './cats/cats.service'; @Module({ controllers: [CatsController], providers: [CatsService], }) export class AppModule {}
import { Controller, Get } from '@nestjs/common'; import { CatsService } from './cats.service'; import { Cat } from './interfaces/cat.interface'; @Controller('cats') export class CatsController { constructor(private catsService: CatsService) {} // 通过构造函数注入 @Get() async findAll(): Promise<Cat[]> { return this.catsService.findAll(); } }
注意:在 NestJS 中,通过 @Injectable() 装饰器定义的服务(Provider),如果没有在模块的 exports 数组中导出,默认仅在当前模块内部可用,其他模块无法注入使用。这是由 NestJS 的模块隔离机制决定的。若要让服务被其他模块使用,必须在当前模块的 exports 中显式导出,且其他模块需要 imports 该模块。或者将模块标记为全局模块:
如果服务所在的模块被标记为 @Global(),且服务在 exports 中导出,则该服务会成为全局服务,无需在其他模块中重复导入模块即可注入使用。
在 NestJS 中,虽然 IOC(控制反转)和 DI(依赖注入)系统主要用于管理 Service 等业务逻辑组件,但理论上可以通过 DI 注入 Controller。不过这种做法并不符合 NestJS 的设计理念,可能会导致一系列问题,但技术上是可行的。
Nest 的 ModuleRef 是一个用于动态获取依赖项的工具类,它与依赖注入(DI)并不冲突,是对 DI 系统的补充——解决了 DI 无法覆盖的动态依赖获取场景。
Nest 的核心依赖注入(DI)系统通过 @Injectable()、constructor 注入等方式,实现了静态依赖管理:
但实际开发中,有很多动态场景是纯 DI 无法满足的,例如:
scope: Scope.TRANSIENT)服务;ModuleRef 的核心作用:动态访问依赖ModuleRef 提供了一系列方法,允许在运行时动态获取模块中注册的依赖项,突破了 DI 静态注入的限制。
AdminProcessor 或 UserProcessor),纯 DI 无法在构造函数中根据运行时参数选择依赖,此时可用 ModuleRef:// processors.ts @Injectable() export class AdminProcessor { handle() { return '管理员处理逻辑'; } } @Injectable() export class UserProcessor { handle() { return '普通用户处理逻辑'; } } // app.service.ts import { Injectable, ModuleRef } from '@nestjs/common'; @Injectable() export class AppService { // 注入 ModuleRef constructor(private moduleRef: ModuleRef) {} // 运行时根据角色动态获取处理器 process(role: string) { // 动态获取服务:根据角色选择 AdminProcessor 或 UserProcessor const Processor = role === 'admin' ? AdminProcessor : UserProcessor; const processor = this.moduleRef.get(Processor); // 动态获取实例 return processor.handle(); } }
@Injectable() 类的构造函数中注入依赖,但如果在静态方法或普通函数中需要依赖,可通过 ModuleRef 手动获取:import { ModuleRef } from '@nestjs/common'; // 普通工具函数(非注入类) export function utilsFn(moduleRef: ModuleRef) { // 从 ModuleRef 中获取服务 const configService = moduleRef.get(ConfigService); return configService.get('APP_PORT'); } // 在注入类中传递 ModuleRef 给工具函数 @Injectable() export class AppService { constructor(private moduleRef: ModuleRef) {} getPort() { return utilsFn(this.moduleRef); // 传递 ModuleRef 到普通函数 } }
Scope.TRANSIENT 或 Scope.REQUEST)
Nest 中默认服务是单例(Scope.DEFAULT),但如果服务被标记为 Scope.TRANSIENT(每次注入都是新实例)或 Scope.REQUEST(每个请求一个实例),纯 DI 注入的是“当前上下文的实例”,而 ModuleRef 可灵活获取:@Injectable({ scope: Scope.TRANSIENT }) // 非单例服务 export class TransientService { id = Math.random(); // 每次实例化 ID 不同 } @Injectable() export class AppService { constructor( private moduleRef: ModuleRef, private transientService: TransientService, // DI 注入的是一个实例 ) {} getInstances() { const instance1 = this.moduleRef.get(TransientService); // 新实例 const instance2 = this.moduleRef.get(TransientService); // 另一个新实例 return { diInstance: this.transientService.id, instance1: instance1.id, instance2: instance2.id, // 三个 ID 均不同 }; } }
new 一个类,且该类本身有依赖(需要 DI 注入),ModuleRef 的 create 方法可自动为其注入依赖:@Injectable() export class DependencyService { getValue() { return '依赖的值'; } } // 需要手动实例化的类(有依赖) export class ManualClass { constructor(private dep: DependencyService) {} // 依赖需要注入 doSomething() { return this.dep.getValue(); } } @Injectable() export class AppService { constructor(private moduleRef: ModuleRef) {} createManualInstance() { // 手动实例化 ManualClass,并自动注入其依赖 const manualInstance = this.moduleRef.create(ManualClass); return manualInstance.doSomething(); // 正常调用,依赖已注入 } }
ModuleRef 与 DI 的关系ModuleRef 是补充:用于 DI 无法覆盖的动态场景,它依赖 DI 系统(基于模块注册的元数据),但提供了运行时动态访问的能力。简单说:DI 解决“静态依赖注入”,ModuleRef 解决“动态依赖获取”,二者相辅相成。
ModuleRef 的核心价值是打破 DI 的静态限制,允许在运行时动态获取、创建依赖实例,适用于动态决策、非注入上下文、非单例服务等场景。它并不替代 DI,而是让 Nest 的依赖管理更加灵活。在实际开发中,应优先使用 DI,仅在必要时引入 ModuleRef。
ArgumentsHost 类提供了检索传递给处理程序参数的方法。它允许选择合适的上下文(如 HTTP、RPC(微服务)或 WebSockets)来获取参数。框架会在需要访问参数的地方提供 ArgumentsHost 的实例,通常以 host 参数的形式引用。例如, 异常过滤器的 catch() 方法在被调用时会传入一个 ArgumentsHost 实例。
我们使用 switchToHttp() 方法重写前面的示例。host.switchToHttp() 辅助调用会返回一个适用于 HTTP 应用程序上下文的 HttpArgumentsHost 对象。该对象有两个实用方法可用于提取所需对象。在此示例中,我们还使用了 Express 类型断言来返回原生 Express 类型的对象:
const ctx = host.switchToHttp(); // 返回HTTP程序的HttpArgumentsHost对象 const request = ctx.getRequest<Request>(); const response = ctx.getResponse<Response>();
类似地,WsArgumentsHost 和 RpcArgumentsHost 也提供了分别在WebSocket和微服务 上下文中返回相应对象的方法。
ExecutionContext 继承自 ArgumentsHost,提供了关于当前执行过程的额外细节。与 ArgumentsHost 类似,Nest 会在你可能需要的地方提供 ExecutionContext 实例,例如在 守卫 的 canActivate() 方法和 拦截器 的 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 上下文中,如果当前处理的请求是绑定到 CatsController 上 create() 方法的 POST 请求,getHandler() 将返回 create() 方法的引用,而 getClass() 将返回 CatsController 类 (而非实例)。
Nest 提供了通过 Reflector.createDecorator 方法创建的装饰器以及内置 @SetMetadata() 装饰器将自定义元数据附加到路由处理程序的能力。在本节中,我们将比较这两种方法,并了解如何从守卫或拦截器内部访问元数据。
要使用 Reflector#createDecorator 创建强类型装饰器,我们需要指定类型参数。例如,让我们创建一个接受字符串数组作为参数的 Roles 装饰器。
// roles.decorator.ts import { Reflector } from '@nestjs/core'; export const Roles = Reflector.createDecorator<string[]>();
这里的 Roles 装饰器是一个接收 string[] 类型单一参数的函数。
现在要使用这个装饰器,我们只需用它来注解处理器:
cats.controller.ts @Post() @Roles(['admin']) async create(@Body() createCatDto: CreateCatDto) { this.catsService.create(createCatDto); }
这里我们将 Roles 装饰器元数据附加到 create() 方法上,表明只有具有 admin 角色的用户才被允许访问此路由。
为了访问路由的角色(自定义元数据),我们将再次使用 Reflector 辅助类。Reflector 可以通过常规方式注入到类中:
// roles.guard.ts @Injectable() export class RolesGuard { constructor(private reflector: Reflector) {} }
提示: Reflector 类是从 @nestjs/core 包导入的。 现在,要读取处理程序的元数据,请使用 get() 方法:
// roles.guard.ts // 控制器方法 要求的角色 const roles = this.reflector.get(Roles, context.getHandler()); // 获取当前角色,一般写在request对象上方便传递 const currentRoles = getRoles(request); // @todo: 角色对比,失败则返回false
Reflector#get 方法允许我们通过传入两个参数轻松访问元数据:一个装饰器引用和一个用于检索元数据的上下文 (装饰器目标)。在本例中,指定的装饰器是 Roles(请参考上面的 roles.decorator.ts 文件)。上下文由 context.getHandler() 调用提供,这会提取当前处理的路由处理程序的元数据。记住,getHandler() 会给我们一个路由处理函数的引用 。
或者,我们也可以通过将元数据应用到控制器级别来组织控制器,这将应用于控制器类中的所有路由。
// cats.controller.ts @Roles(['admin']) @Controller('cats') export class CatsController {}
在这种情况下,为了提取控制器元数据,我们传递 context.getClass() 作为第二个参数(以提供控制器类作为元数据提取的上下文),而不是 context.getHandler():
// roles.guard.ts 守卫 // 要求的角色 const roles = this.reflector.get(Roles, context.getClass()); // 获取当前角色,一般写在request对象上方便传递 const currentRoles = getRoles(request); // @todo: 角色对比,失败则返回false
之前提到过typescript装饰器是nestjs装饰器的基础,这里介绍以下typescript装饰: TypeScript 装饰器(Decorator)是一种特殊类型的声明,它能够修饰类、方法、属性、参数等,为其添加额外的元数据或功能。装饰器本质上是一个函数,在代码编译或运行时被调用,用于修改被装饰对象的行为。
装饰器是 TypeScript 对 JavaScript 的扩展(目前处于 ECMAScript 提案阶段),广泛用于框架(如 NestJS、Angular)中实现依赖注入、路由定义、数据校验等功能。
装饰器通过 @装饰器名称 的语法应用于目标对象,支持以下类型:
装饰器在代码编译后、类实例化前执行(属于“编译时”或“类定义时”执行),而非运行时。 这意味着装饰器的逻辑会在类被定义时就生效,而非每次实例化时调用。
作用于类本身,用于修改类的定义(如添加静态属性、方法,或修改构造函数)。
语法:
// 类装饰器函数,参数为类的构造函数 function 装饰器名称(constructor: Function) { // 装饰逻辑 } @装饰器名称 class 类名 { ... }
示例:为类添加静态属性和实例方法
// 定义类装饰器:为类添加版本信息和打印方法 function addVersion(version: string) { // 返回装饰器函数(支持参数化) return function (constructor: Function) { // 添加静态属性 constructor.prototype.version = version; // 添加实例方法 constructor.prototype.logVersion = function () { console.log(`版本:${this.version}`); }; }; } // 应用装饰器 @addVersion('1.0.0') class MyClass { name: string; constructor(name: string) { this.name = name; } } // 使用 const instance = new MyClass('测试'); instance.logVersion(); // 输出:版本:1.0.0 console.log(instance.version); // 输出:1.0.0
作用于类的方法,用于修改方法的行为(如添加日志、缓存、权限校验等)。
语法:
// 方法装饰器函数 function 装饰器名称( target: any, // 对于实例方法:类的原型对象;对于静态方法:类的构造函数 propertyKey: string, // 方法名 descriptor: PropertyDescriptor // 方法的属性描述符 ) { // 装饰逻辑 } class 类名 { @装饰器名称 方法名() { ... } }
示例:为方法添加日志记录
// 方法装饰器:打印方法调用前后的日志 function logMethod(target: any, methodName: string, descriptor: PropertyDescriptor) { // 保存原始方法 const originalMethod = descriptor.value; // 重写方法 descriptor.value = function (...args: any[]) { console.log(`[日志] 方法 ${methodName} 开始调用,参数:`, args); // 调用原始方法 const result = originalMethod.apply(this, args); console.log(`[日志] 方法 ${methodName} 调用结束,返回值:`, result); return result; }; } class Calculator { @logMethod add(a: number, b: number): number { return a + b; } } // 使用 const calc = new Calculator(); calc.add(2, 3); // 输出: // [日志] 方法 add 开始调用,参数: [2, 3] // [日志] 方法 add 调用结束,返回值: 5
作用于类的属性,用于定义属性的元数据(如验证规则、数据库字段映射等)。
语法:
// 属性装饰器函数 function 装饰器名称( target: any, // 类的原型对象(实例属性)或构造函数(静态属性) propertyKey: string // 属性名 ) { // 装饰逻辑(通常用于添加元数据) } class 类名 { @装饰器名称 属性名: 类型; }
示例:记录属性的元数据(结合 reflect-metadata 库)
import 'reflect-metadata'; // 需要安装:npm install reflect-metadata // 定义元数据键 const MIN_LENGTH_KEY = 'minLength'; // 属性装饰器:为属性添加“最小长度”元数据 function MinLength(length: number) { return function (target: any, propertyKey: string) { Reflect.defineMetadata(MIN_LENGTH_KEY, length, target, propertyKey); }; } class User { @MinLength(3) // 用户名最小长度为 3 username: string; constructor(username: string) { this.username = username; } } // 读取元数据 const user = new User('abc'); const minLength = Reflect.getMetadata(MIN_LENGTH_KEY, user, 'username'); console.log(`username 最小长度:${minLength}`); // 输出:3
作用于方法的参数,用于为参数添加元数据(如标记必填参数、依赖注入标识等)。
语法:
// 参数装饰器函数 function 装饰器名称( target: any, // 类的原型对象或构造函数 propertyKey: string, // 方法名 parameterIndex: number // 参数在方法参数列表中的索引 ) { // 装饰逻辑 } class 类名 { 方法名(@装饰器名称 参数名: 类型) { ... } }
示例:标记必填参数并验证
import 'reflect-metadata'; const REQUIRED_KEY = 'required'; // 参数装饰器:标记参数为必填 function Required(target: any, methodName: string, paramIndex: number) { // 获取当前方法已标记的必填参数索引列表 const requiredParams: number[] = Reflect.getMetadata(REQUIRED_KEY, target, methodName) || []; requiredParams.push(paramIndex); // 保存元数据 Reflect.defineMetadata(REQUIRED_KEY, requiredParams, target, methodName); } // 方法装饰器:验证必填参数 function Validate(target: any, methodName: string, descriptor: PropertyDescriptor) { const originalMethod = descriptor.value; descriptor.value = function (...args: any[]) { // 获取该方法的必填参数索引 const requiredParams: number[] = Reflect.getMetadata(REQUIRED_KEY, target, methodName) || []; // 检查必填参数是否为空 requiredParams.forEach((index) => { if (args[index] === undefined || args[index] === null) { throw new Error(`参数 ${index} 为必填项`); } }); return originalMethod.apply(this, args); }; } class UserService { @Validate createUser(@Required username: string, age?: number) { return { username, age }; } } // 使用 const userService = new UserService(); userService.createUser('张三'); // 正常执行 userService.createUser(undefined); // 抛出错误:参数 0 为必填项
当多个装饰器应用于同一目标时,执行顺序为:
@enumerable)后方法本身。示例:
function Decorator1() { console.log('类装饰器 1'); return (t: any) => {}; } function Decorator2() { console.log('类装饰器 2'); return (t: any) => {}; } @Decorator1() @Decorator2() class MyClass { @PropDecorator() prop: string; @MethodDecorator() method(@ParamDecorator1() a: any, @ParamDecorator2() b: any) {} } // 执行顺序: // ParamDecorator1 → ParamDecorator2 → PropDecorator → MethodDecorator → Decorator2 → Decorator1
@Controller()、@Injectable() 实现依赖注入;Angular 的 @Component() 定义组件。class-validator 的 @IsString()、@Min() 用于数据校验。TypeScript 中使用装饰器需在 tsconfig.json 中启用配置:
{ "compilerOptions": { "target": "ES5", "experimentalDecorators": true, // 启用装饰器 "emitDecoratorMetadata": true // 允许发射装饰器元数据(配合 reflect-metadata 库) } }
TypeScript 装饰器是一种强大的元编程工具,通过修饰类、方法、属性等,能够在不修改原始代码的情况下扩展功能。其核心价值在于分离业务逻辑与横切关注点(如日志、校验、权限),广泛应用于现代前端框架和 Node.js 服务端开发中。
理解装饰器的工作原理和执行顺序,有助于更好地使用框架提供的装饰器,或自定义符合业务需求的装饰器。
Nest.js 的核心原理其实就是通过装饰器给类、方法、参数或者对象添加元数据(nestjs通过npm包 Reflect Metadata维护这些数据),然后初始化的时候取出这些元数据,进行依赖的分析,然后创建对应的实例对象;和Java的Spring Boot注解一样,都是 根据 装饰器/注解这类标记 来创建 应用服务 的模式, 装饰器和注解本质上都是 “元数据标记工具”。 关于依赖注入DI,二者的核心都是框架容器维护一个 “组件注册表”,根据元数据查找并注入依赖。本质都是 “约定优于配置” 的元编程思想:
无论是 Nest.js 的装饰器还是 Spring Boot 的注解,核心都是 “元编程”:
这种思想在现代框架中非常普遍(如 Angular、Django 等),本质是用 “声明式代码” 替代 “命令式代码”,减少重复劳动,提高开发效率。
在 Nest 中,微服务本质上是一个使用TCP作为请求的程序(同Spring boot的微服务类似)。
要开始构建微服务,首先需要安装所需包:
npm i --save @nestjs/microservices
两个服务 服务A提供计算(也提供HTTP服务),服务B响应浏览器的HTTP请求,并且在部分请求中请求A来计算,写在一个项目里吧
注册服务(也可以拆分为两个项目,更推荐)
// main.ts import { NestFactory } from '@nestjs/common'; import { MicroserviceOptions, Transport } from '@nestjs/microservices'; import { AppModule } from './app.module'; import { MathModule } from './math/math.module'; async function bootstrap() { // 1. 启动服务A(同时提供HTTP和TCP服务) // 1.1 启动HTTP服务(端口3001) const mathHttpApp = await NestFactory.create(MathModule); await mathHttpApp.listen(3001); console.log('服务A的HTTP服务已启动,访问 http://localhost:3001'); // 1.2 启动TCP微服务(端口4001) const mathMicroservice = await NestFactory.createMicroservice<MicroserviceOptions>( MathModule, { transport: Transport.TCP, options: { port: 4001 }, }, ); await mathMicroservice.listen(); console.log('服务A的TCP微服务已启动,监听端口4001'); // 2. 启动服务B(HTTP服务,端口3000) const app = await NestFactory.create(AppModule); await app.listen(3000); console.log('服务B的HTTP服务已启动,访问 http://localhost:3000'); } bootstrap();
服务A:
// math.module.ts import { Module } from '@nestjs/common'; import { MathController } from './math.controller'; import { MathService } from './math.service'; @Module({ controllers: [MathController], providers: [MathService], }) export class MathModule {} // math.service.ts import { Injectable } from '@nestjs/common'; @Injectable() export class MathService { // 加法计算 add(a: number, b: number): number { return a + b; } // 乘法计算 multiply(a: number, b: number): number { return a * b; } } // math.controller.ts import { Controller, Get, Query } from '@nestjs/common'; import { MessagePattern } from '@nestjs/microservices'; import { MathService } from './math.service'; @Controller() export class MathController { constructor(private readonly mathService: MathService) {} // 服务A的HTTP接口(供直接HTTP访问) @Get('add') httpAdd(@Query('a') a: number, @Query('b') b: number) { const result = this.mathService.add(Number(a), Number(b)); return { from: '服务A的HTTP接口', operation: '加法', a, b, result }; } @Get('multiply') httpMultiply(@Query('a') a: number, @Query('b') b: number) { const result = this.mathService.multiply(Number(a), Number(b)); return { from: '服务A的HTTP接口', operation: '乘法', a, b, result }; } // 服务A的TCP微服务接口(供服务B调用) @MessagePattern('add') tcpAdd(data: { a: number; b: number }): number { return this.mathService.add(data.a, data.b); } @MessagePattern('multiply') tcpMultiply(data: { a: number; b: number }): number { return this.mathService.multiply(data.a, data.b); } }
服务B:
// api.module.ts import { Module } from '@nestjs/common'; import { ClientsModule, Transport } from '@nestjs/microservices'; import { ApiController } from './api.controller'; import { ApiService } from './api.service'; @Module({ imports: [ ClientsModule.register([ { name: 'MATH_MICROSERVICE', // 客户端标识 transport: Transport.TCP, options: { host: 'localhost', port: 4001 }, // 服务A的TCP端口 }, ]), ], controllers: [ApiController], providers: [ApiService], }) export class ApiModule {} // api.service.ts import { Injectable } from '@nestjs/common'; import { ClientProxy } from '@nestjs/microservices'; import { Inject } from '@nestjs/common'; @Injectable() export class ApiService { // 注入服务A的TCP客户端 constructor(@Inject('MATH_MICROSERVICE') private readonly mathClient: ClientProxy) {} // 调用服务A的加法(TCP) async add(a: number, b: number): Promise<number> { return this.mathClient.send<number>('add', { a, b }).toPromise(); } // 调用服务A的乘法(TCP) async multiply(a: number, b: number): Promise<number> { return this.mathClient.send<number>('multiply', { a, b }).toPromise(); } } // api.controller.ts import { Controller, Get, Query } from '@nestjs/common'; import { ApiService } from './api.service'; @Controller() export class ApiController { constructor(private readonly apiService: ApiService) {} // 服务B的HTTP接口(内部调用服务A的TCP接口) @Get('add') async add(@Query('a') a: number, @Query('b') b: number) { const result = await this.apiService.add(Number(a), Number(b)); return { from: '服务B的HTTP接口(调用服务A的TCP)', operation: '加法', a, b, result }; } @Get('multiply') async multiply(@Query('a') a: number, @Query('b') b: number) { const result = await this.apiService.multiply(Number(a), Number(b)); return { from: '服务B的HTTP接口(调用服务A的TCP)', operation: '乘法', a, b, result }; } }
另外假设我现在要加一个限制 A服务只给同一ip的TCP请求服务:
要实现 “服务 A 只响应同一 IP 的 TCP 请求”,可以在服务 A 的微服务中添加 IP 验证逻辑,通过拦截器或守卫检查客户端的 IP 地址, 仅允许本地 IP(如127.0.0.1)或指定 IP 段的请求。
安装依赖,使用TypeORM作为ORM框架
yarn add @nestjs/typeorm typeorm mysql2 -S
【注意】 typeORM的CRUD操作返回Promise,需要在控制器中使用 await 等待来获取正确结果,但是这样也会导致服务方法也要加async,进而导致返回值也是Promise,最终导致整个请求链路上都是await+async的组合。这也是Nest进行IO操作的标准写法,需要加大量的await/async。
【建议】将数据库配置模块(不仅有数据库配置,还有全部实体类)单独抽离出来,设置为全局模块,这样可以方便的在其他模块中使用。
// app.module.ts 根节点配置 数据库连接 import { Module } from '@nestjs/common'; import { AppController } from './app.controller'; import { AppService } from './app.service'; import { TestController } from './test/test.controller'; import { ArticleModule } from './article/article.module'; import { TestService } from './test/test.service'; import { TypeOrmModule } from '@nestjs/typeorm'; @Module({ imports: [ ArticleModule, TypeOrmModule.forRoot({ type: 'mysql', host: 'localhost', port: 3306, username: 'jinxiaochi', password: 'root123@', database: 'myblog', poolSize: 10, entities: [], autoLoadEntities: true, }), ], controllers: [AppController, TestController], providers: [AppService, TestService ], }) export class AppModule { } // 创建实体类 /entity/article.entity.ts import { Entity, Column, PrimaryGeneratedColumn } from 'typeorm'; @Entity('article') export class Article { @PrimaryGeneratedColumn() articleId: string; @Column() title: string; @Column() content: string; @Column() content_type: string; @Column() type: string; @Column() tag: string; @Column('date') replease_time: string; @Column('date') update_time: string; @Column() status: string; @Column({ name: 'describ' }) describe: string; } // 使用模块中 注册实体类的包装类 import { Module } from '@nestjs/common'; import { ArticleService } from './article.service'; import { ArticleController } from './article.controller'; import { TypeOrmModule } from '@nestjs/typeorm'; import { Article } from './entities/article.entity'; @Module({ imports: [TypeOrmModule.forFeature([Article])], // 注册实体类的包装类 controllers: [ArticleController], providers: [ArticleService], }) export class ArticleModule {} // 在具体服务中使用 article.service.ts import { Injectable } from '@nestjs/common'; import { CreateArticleDto } from './dto/create-article.dto'; import { UpdateArticleDto } from './dto/update-article.dto'; import { InjectRepository } from '@nestjs/typeorm'; import { Article } from './entities/article.entity'; import { Repository } from 'typeorm'; @Injectable() export class ArticleService { constructor(@InjectRepository(Article) private articleRepository: Repository<Article>) { } // 【注意】 typeORM的CRUD操作返回Promise,需要在控制器中使用 await 等待来获取正确结果(但是这样也会导致服务方法也要加async,进而导致返回值也是Promise,最终导致整个请求链路上都是await+async的组合) async findOne(id: number) { return await this.articleRepository.findOne({ where: { articleId: String(id) }}); } }
安装依赖,使用Redis作为缓存(默认情况下所有内容都存储在内存中)
yarn add @nestjs/cache-manager cache-manager @keyv/redis -S
// 注册 缓存模块 import { Module } from '@nestjs/common'; import { AppController } from './app.controller'; import { AppService } from './app.service'; import { TestController } from './test/test.controller'; import { ArticleModule } from './article/article.module'; import { TestService } from './test/test.service'; import { CacheModule } from '@nestjs/cache-manager'; import { createKeyv, Keyv } from '@keyv/redis'; @Module({ imports: [ ArticleModule, // 注册缓存模块 CacheModule.registerAsync({ isGlobal: true, // 全局可用 useFactory: async () => { return { stores: [ // redis 存储(缓存) createKeyv({ url: 'redis://127.0.0.1:6379', password: 'root@123', database: 2, // redis 库编号 }), ], }; }, }), ], controllers: [AppController, TestController], providers: [AppService, TestService ], }) export class AppModule { } // 使用缓存(有两种使用方式) article.controller.ts import { Controller, Get, Post, Body, Patch, Param, Delete, Logger, Inject, UseInterceptors } from '@nestjs/common'; import { ArticleService } from './article.service'; import { CreateArticleDto } from './dto/create-article.dto'; import { UpdateArticleDto } from './dto/update-article.dto'; import { CACHE_MANAGER, CacheInterceptor, CacheKey, CacheTTL } from '@nestjs/cache-manager'; import type { Cache } from 'cache-manager'; @Controller('article') export class ArticleController { // 使用方式1:步骤1,注入 constructor(private readonly articleService: ArticleService, @Inject(CACHE_MANAGER) private cacheManager: Cache) {} private readonly logger = new Logger(ArticleController.name); // 使用方式2:使用缓存拦截器的装饰器,并设置缓存key和缓存时间 // 接口响应缓存,默认缓存时间为1000ms,这里设置为60_000ms @CacheTTL(60000) @CacheKey('key_findAll') @UseInterceptors(CacheInterceptor) @Get() async findAll() { this.logger.error('findAll=============='); // throw new Error('test error'); return await this.articleService.findAll(); } @Get(':id') async findOne(@Param('id') id: string) { this.logger.warn(`warnOne==============: ${id}`); this.logger.log(`findOne==============: ${id}`); // 使用方式1:步骤2,执行set和get操作 const cache = await this.cacheManager.get('key_'+id); if (cache) { this.logger.log(`findOne Cache==============: ${ JSON.stringify(cache) }`); return cache; } const result = await this.articleService.findOne(+id); this.logger.log(`findOne DataBase==============: ${ JSON.stringify(result) }`); await this.cacheManager.set('key_'+id, { data: '缓存数据' }, 60_000); //缓存10秒 return result; } }
@nestjs/config 包内部使用了 dotenv 库,它允许你使用 .env 文件来存储环境变量。
npm i --save @nestjs/config
更多自定义配置(如自定义配置文件路径、自定义加载类、配置校验等)可以看官网文档 https://docs.nestjs.cn/techniques/configuration
// .env 的文件创建 NEST_PORT = 3000 NEST_PROJECT = APP_DEMO // 全局配置 import { Module } from '@nestjs/common'; import { AppController } from './app.controller'; import { AppService } from './app.service'; import { TestController } from './test/test.controller'; import { ArticleModule } from './article/article.module'; import { TestService } from './test/test.service'; import { TypeOrmModule } from '@nestjs/typeorm'; import { CacheModule } from '@nestjs/cache-manager'; import { createKeyv, Keyv } from '@keyv/redis'; import { ConfigModule } from '@nestjs/config'; @Module({ imports: [ // 全局配置环境变量模块 ConfigModule.forRoot({ isGlobal: true, }), //.... ], controllers: [AppController, TestController], providers: [AppService, TestService ], }) export class AppModule { } // 要使用的地方 (Controller或Service等任何其他地方) console.log("env: "+ process.env.NEST_PROJECT);
https://docs.nestjs.cn/techniques/logger
nestjs 内置日志只打印到控制台,如果需要将日志输出到文件,可以使用 winston 日志库。
npm install winston nest-winston winston-daily-rotate-file --save
在文件中配置:
// winston.config.ts 日志配置文件 import winston from 'winston'; import { createLogger, transports, format, } from 'winston'; import { utilities, WinstonModule } from 'nest-winston'; import 'winston-daily-rotate-file'; // 用于存储日志到文件 import path from 'path'; // 日志文件存储路径(建议放在项目根目录的 logs 文件夹) const logDir = path.join(__dirname, '../../logs'); export const WinstonConfig = { // 日志选项 transports: [ new winston.transports.Console({ level: 'info', // 字符串拼接 format: winston.format.combine( winston.format.timestamp(), utilities.format.nestLike(), ), }), // error日志存储到/logs/error-日期.log文件中 new winston.transports.DailyRotateFile({ level: 'error', dirname: logDir, filename: 'error-%DATE%.log', datePattern: 'YYYY-MM-DD', zippedArchive: true, maxSize: '20m', maxFiles: '14d', format: winston.format.combine( format.timestamp({ format: 'YYYY-MM-DD HH:mm:ss.SSS' }), // 配置时间格式 format.errors({ stack: true }), // 开启错误堆栈 format.printf(({ timestamp, level, context, message, stack }) => { return `[${timestamp}] ${level}: [${context || 'default'}] ${message}\n${stack || ''}`; }), ), }), // info级别及以上日志存储到/logs/info-日期.log文件中 new winston.transports.DailyRotateFile({ level: 'info', dirname: logDir, filename: 'info-%DATE%.log', datePattern: 'YYYY-MM-DD', zippedArchive: true, // 文件大小 maxSize: '20m', // 最多14 天 maxFiles: '14d', format: winston.format.combine( format.timestamp({ format: 'YYYY-MM-DD HH:mm:ss.SSS' }), format.printf(({ timestamp, level, context, message }) => { return `[${timestamp}] ${level}: [${context || 'default'}] ${message}`; }), ), }), ], } // main.ts 中使用 import { AppModule } from './app.module'; import { NestFastifyApplication } from '@nestjs/platform-fastify'; import { createLogger } from 'winston'; import { WinstonModule } from 'nest-winston'; import { WinstonConfig } from './config/winston.config'; async function bootstrap() { // 日志配置 const instance = createLogger(WinstonConfig); const app = await NestFactory.create<NestFastifyApplication>(AppModule, { logger: WinstonModule.createLogger(instance), }); await app.listen(process.env.PORT ?? 9000); } bootstrap();
在 NestJS 中,DTO(数据传输对象)的入参合法性合法性校验通常结合 class-validator(提供校验装饰器)和 class-transformer(负责类型转换)实现。
以下是一个完整示例:
# 安装校验核心库 npm install class-validator class-transformer
以“创建用户”接口为例,定义 CreateUserDto 并添加校验规则:
// src/user/dto/create-user.dto.ts import { IsString, IsEmail, MinLength, MaxLength, IsOptional, IsInt, Min } from 'class-validator'; // 导入校验装饰器 export class CreateUserDto { // 用户名:必须是字符串,长度 2-20 @IsString({ message: '用户名必须是字符串' }) @MinLength(2, { message: '用户名至少 2 个字符' }) @MaxLength(20, { message: '用户名最多 20 个字符' }) username: string; // 邮箱:必须是合法邮箱格式 @IsEmail({}, { message: '请输入合法的邮箱地址' }) email: string; // 密码:必须是字符串,至少 6 位 @IsString({ message: '密码必须是字符串' }) @MinLength(6, { message: '密码至少 6 位' }) password: string; // 年龄:可选参数,必须是整数且至少 0 @IsOptional() // 标记为可选参数 @IsInt({ message: '年龄必须是整数' }) @Min(0, { message: '年龄不能为负数' }) age?: number = 18; // 可选字段用 ? 标记,并设置默认值 }
通过 @UsePipes() 装饰器启用全局或局部校验管道(ValidationPipe):
在 main.ts 中配置全局校验管道,对所有接口生效:
// src/main.ts import { NestFactory } from '@nestjs/common'; import { AppModule } from './app.module'; import { ValidationPipe } from '@nestjs/common'; // 导入 Nest 内置的校验管道 async function bootstrap() { const app = await NestFactory.create(AppModule); // 启用全局校验管道 app.useGlobalPipes( new ValidationPipe({ whitelist: true, // 自动移除 DTO 中未定义的字段(防止恶意传入额外参数) forbidNonWhitelisted: true, // 若传入未定义的字段,直接报错 transform: true, // 自动将请求参数转换为 DTO 类型(如字符串转数字) }), ); await app.listen(3000); } bootstrap();
// src/user/user.controller.ts import { Controller, Post, Body, UsePipes, ValidationPipe } from '@nestjs/common'; import { CreateUserDto } from './dto/create-user.dto'; import { UserService } from './user.service'; @Controller('users') // 对整个控制器启用校验(也可单独对某个接口添加) @UsePipes(new ValidationPipe({ whitelist: true })) export class UserController { constructor(private readonly userService: UserService) {} @Post() create(@Body() createUserDto: CreateUserDto) { // 若校验失败,会自动抛出 400 错误,不会执行到这里 return this.userService.create(createUserDto); } }
当客户端传入不符合规则的参数时,NestJS 会自动返回校验错误:
// 请求体 { "username": "a", // 长度不足 2 位 "email": "invalid-email", // 非合法邮箱 "password": "123", // 长度不足 6 位 "age": -5, // 年龄为负数 "extra": "恶意字段" // DTO 中未定义的字段 }
{ "statusCode": 400, "message": [ "用户名至少 2 个字符", "请输入合法的邮箱地址", "密码至少 6 位", "年龄不能为负数", "属性 extra 不允许" // 因 forbidNonWhitelisted: true 触发 ], "error": "Bad Request" }
| 装饰器 | 作用 | 示例 |
| --------------- | ------------------ | ---------------------------------------- |
| @IsString() | 必须是字符串 | @IsString({ message: '错误信息' }) |
| @IsNumber() | 必须是数字 | @IsNumber({}, { message: '错误信息' }) |
| @IsEmail() | 必须是合法邮箱 | @IsEmail({}, { message: '错误信息' }) |
| @MinLength(n) | 字符串最小长度 | @MinLength(2) |
| @MaxLength(n) | 字符串最大长度 | @MaxLength(20) |
| @Min(n) | 数字最小值 | @Min(0) |
| @Max(n) | 数字最大值 | @Max(120) |
| @IsOptional() | 字段可选(可省略) | @IsOptional() |
| @IsEnum(Enum) | 必须是枚举值之一 | @IsEnum(RoleEnum) |
| @IsUrl() | 必须是合法 URL | @IsUrl() |
class-validator 装饰器定义 DTO 的校验规则。ValidationPipe(全局或局部)使校验生效。这种方式既能保证接口入参的合法性,又能统一错误格式,是 NestJS 推荐的最佳实践。
Q:NestJS 的官方限流库 @nestjs/throttler,能不能直接用于 Fastify 适配器?
A:简短回答:
✅ 可以用! 但需要注意两个点:
- 它是 框架无关的(HTTP 层无关),可以用于
Express或Fastify;- 但是默认取 IP 的方式在 Fastify 下可能不准,需要手动指定
getTracker。
npm install @nestjs/throttler
app.module.ts)import { Module } from '@nestjs/common' import { ThrottlerModule } from '@nestjs/throttler' import { APP_GUARD } from '@nestjs/core' import { ThrottlerGuard } from '@nestjs/throttler' import { AppController } from './app.controller' @Module({ imports: [ ThrottlerModule.forRoot([ { ttl: 60, // 时间窗口(秒) limit: 10, // 同一用户在 ttl 秒内最多请求次数 }, ]), ], controllers: [AppController], providers: [ { provide: APP_GUARD, useClass: ThrottlerGuard, // 全局启用限流 }, ], }) export class AppModule {}
默认情况下,@nestjs/throttler 的 ThrottlerGuard 在 Fastify 下可能拿不到正确的 IP(会是 undefined),要通过重写 getTracker() 来修复:
import { ThrottlerGuard } from '@nestjs/throttler' import { Injectable, ExecutionContext } from '@nestjs/common' @Injectable() export class FastifyThrottlerGuard extends ThrottlerGuard { protected getTracker(req: Record<string, any>): string { // Fastify 中 req.ip 是正确的 return req.ip } protected getRequestResponse(context: ExecutionContext) { const http = context.switchToHttp() return { req: http.getRequest(), res: http.getResponse() } } }
然后在 AppModule 中替换掉默认的 Guard:
providers: [ { provide: APP_GUARD, useClass: FastifyThrottlerGuard, }, ],
@nestjs/throttler 支持局部限流:
import { Controller, Get } from '@nestjs/common' import { Throttle } from '@nestjs/throttler' @Controller('auth') export class AuthController { @Get('login') @Throttle(5, 10) // 10 秒内最多 5 次 login() { return { msg: 'ok' } } @Get('captcha') @Throttle(1, 30) // 30 秒最多一次 captcha() { return { msg: 'captcha' } } }
默认情况下,@nestjs/throttler 存储限流数据在内存中。
如果你的服务多实例部署,可以改用 Redis 存储:
安装:
npm install @nestjs/throttler-storage-redis ioredis
配置:
import { ThrottlerModule } from '@nestjs/throttler' import { ThrottlerStorageRedisService } from '@nestjs/throttler-storage-redis' @Module({ imports: [ ThrottlerModule.forRoot([ { ttl: 60, limit: 20, storage: new ThrottlerStorageRedisService({ host: '127.0.0.1', port: 6379, }), }, ]), ], }) export class AppModule {}
| 方式 | 框架级别 | 适配 Fastify | Redis 支持 | 灵活度 |
| --------------------- | ------------ | ------------------------ | ---------- | ------------------- |
| @fastify/rate-limit | Fastify 原生 | ✅ 原生支持 | ✅ 原生支持 | 高(粒度细) |
| @nestjs/throttler | Nest 层 | ✅ 可用(需自定义 Guard) | ✅ 插件支持 | 中(按控制器/方法) |
@nestjs/throttler@fastify/rate-limit(原生快很多)NestJS + Fastify 架构下要做限流(rate limiting),和 Express 版本的 NestJS 是不一样的。
因为底层是 Fastify,所以需要接入 @fastify/rate-limit 插件,而不是 Express 的中间件。
npm install @fastify/rate-limit
main.ts 注册全局限流import { NestFactory } from '@nestjs/core' import { AppModule } from './app.module' import rateLimit from '@fastify/rate-limit' async function bootstrap() { const app = await NestFactory.create(AppModule, { bodyParser: true, // 明确指定 fastify 适配器 rawBody: true }) // 注册限流插件 await app.register(rateLimit, { max: 100, // 每个 IP 最大请求数 timeWindow: '1 minute', // 时间窗口 ban: 2, // 超出多少次后封禁(可选) allowList: ['127.0.0.1'], // 白名单 IP(可选) redis: undefined, // 可挂 Redis 共享计数器 hook: 'onRequest', // 限流触发点,默认 onRequest errorResponseBuilder: (req, context) => { return { code: 503, message: `请求太频繁,请 ${context.after} 秒后再试`, } }, }) await app.listen(3000) } bootstrap()
假如你希望某些接口限流更严格,可以局部注册插件:
import { Controller, Get } from '@nestjs/common' import { FastifyInstance } from 'fastify' import rateLimit from '@fastify/rate-limit' @Controller('auth') export class AuthController { constructor(private readonly app: FastifyInstance) { this.app.register(rateLimit, { max: 5, timeWindow: '10s', keyGenerator: (req) => req.ip + '_auth', // 自定义key }) } @Get('login') async login() { return { message: 'login ok' } } }
⚠️ 注意:如果使用 NestJS 的 FastifyAdapter,
this.app要通过getHttpAdapter().getInstance()拿到 Fastify 实例。
单机没问题,但多节点部署时要用 Redis 存储限流状态:
import rateLimit from '@fastify/rate-limit' import fastifyRedis from '@fastify/redis' await app.register(fastifyRedis, { host: '127.0.0.1' }) await app.register(rateLimit, { max: 100, timeWindow: '1m', redis: app.get<FastifyRedis>('fastify-redis'), })
你也可以做一个 Nest 风格的装饰器,比如:
import { SetMetadata } from '@nestjs/common'; export const RATE_LIMIT_METADATA = 'rate_limit_metadata'; export interface RateLimitOptions { max: number; timeWindow: string; } export const RateLimit = (options: RateLimitOptions) => SetMetadata(RATE_LIMIT_METADATA, options);
然后写一个拦截器,读取这个 metadata,在运行时控制。
// rate-limit.interceptor.ts import { Injectable, NestInterceptor, ExecutionContext, CallHandler, Reflector, } from '@nestjs/common'; import { Observable } from 'rxjs'; import { RATE_LIMIT_METADATA, RateLimitOptions } from './rate-limit.decorator'; import { FastifyRequest } from 'fastify'; @Injectable() export class RateLimitInterceptor implements NestInterceptor { constructor(private reflector: Reflector) {} async intercept(context: ExecutionContext, next: CallHandler): Promise<Observable<any>> { const rateOptions = this.reflector.get<RateLimitOptions>( RATE_LIMIT_METADATA, context.getHandler(), ); if (!rateOptions) { return next.handle(); } const request = context.switchToHttp().getRequest<FastifyRequest>(); // @ts-ignore fastify插件注册后自动注入 const rateLimit = request.rateLimit; if (!rateLimit) return next.handle(); const result = await rateLimit(rateOptions); if (result && result.isExceeded) { throw new Error(`Rate limit exceeded: ${rateOptions.max} requests/${rateOptions.timeWindow}`); } return next.handle(); } }
在 Fastify 模式下初始化时注册限流插件:
// main.ts import { NestFactory } from '@nestjs/core'; import { AppModule } from './app.module'; import rateLimit from '@fastify/rate-limit'; import { FastifyAdapter, NestFastifyApplication } from '@nestjs/platform-fastify'; async function bootstrap() { const app = await NestFactory.create<NestFastifyApplication>( AppModule, new FastifyAdapter(), ); // 注册全局限流插件 await app.register(rateLimit, { // 注意设置 global: false,否则所有路由都会被限流。 global: false, // ❗非常重要,我们只想针对单个路由手动控制 }); await app.listen(3000); } bootstrap();
在根模块中启用:
// app.module.ts import { Module } from '@nestjs/common'; import { APP_INTERCEPTOR } from '@nestjs/core'; import { RateLimitInterceptor } from './rate-limit.interceptor'; @Module({ providers: [ { provide: APP_INTERCEPTOR, useClass: RateLimitInterceptor, }, ], }) export class AppModule {}
在Controller中使用:
import { Controller, Get } from '@nestjs/common'; import { RateLimit } from './rate-limit.decorator'; @Controller('user') export class UserController { @Get('list') @RateLimit({ max: 5, timeWindow: '60s' }) // 在 60 秒内访问 /user/list 超过 5 次,就会抛出错误 getUsers() { return { message: 'ok' }; } }
| 方式 | 优点 | 缺点 | 场景 |
| ------------------------- | -------------- | -------------- | ---------------- |
| 全局限流 (app.register) | 简单、全局统一 | 不灵活 | 小项目或统一策略 |
| 局部限流(单模块) | 可自定义策略 | 配置重复 | 多种限流策略 |
| Redis 分布式限流 | 支持多实例 | 依赖 Redis | 生产环境 |
| 装饰器 + 拦截器自定义 | 灵活 | 需要自己写逻辑 | 高度定制需求 |
文章目录