阅读视图

发现新文章,点击刷新页面。

一文吃透 Nestjs 动态模块之 register、forRoot、forFeature

读完此文,你能更准确的理解动态模块以及清楚register、forRoot、forFeature三者作用,何时使用它们

什么是动态模块

Dynamic modules 官方文档(英文)

Dynamic modules 官方文档(中文镜像)

在 Nest 里,普通(静态)模块可以理解为“写死的一份模块定义”,你只能在 imports: [] 里直接引入模块类;而动态模块允许你在“导入模块时传参”,由模块的静态方法(如 register / forRoot / forFeature返回一个 DynamicModule 对象,Nest 会把这个对象当成模块元数据来编译,从而按你的参数动态生成 providers / exports / imports 等配置。

一句话总结:动态模块 = 可传参的模块工厂,返回 DynamicModule 来决定模块最终提供什么能力。

动态模块作用

动态模块的核心价值是:把“可配置性”变成模块 API 的一部分。也就是说,你不再只能 imports: [SomeModule] 这样“死引入”,而是可以通过 SomeModule.forRoot(...) / forFeature(...) / register(...) 传入参数,让模块在“被导入时”就完成:

  • 根据不同环境/业务场景生成不同的 Provider(例如不同的连接串、不同的开关、不同的策略实现)
  • 导出一组“已经配置好”的能力(让使用方只管注入,不用关心怎么组装)
  • 把模块的配置约束收口到一个入口(避免到处散落 process.env 或重复 new 客户端)

从官方定义看,动态模块本质上就是:一个模块提供一个静态方法,返回 DynamicModule 对象(包含 module / imports / providers / exports / global 等元数据),Nest 会把它当成“模块定义”来编译。

你可以把动态模块理解为:“返回模块定义的工厂函数”,只不过它被约定写成模块类上的静态方法。


什么时候适合用动态模块

动态模块并不是“写 Nest 就必须用”的东西,通常在下面这些场景才值得上:

  • 需要配置且配置会变:例如 JWT、缓存、HTTP 客户端、消息队列、数据库连接等。
  • 需要多实例:同一种能力要按不同名字/用途创建多个实例(例如两个 Redis、两个第三方 API client)。
  • 需要隐藏复杂装配:使用方只想 imports: [...],不想了解内部 provider 如何拼装、如何选择实现。
  • 希望统一约束与默认值:集中处理 option 校验、默认值合并、token 命名、导出策略等。

不适合的情况:

  • 没有任何配置差异,普通静态模块就够了。
  • 配置只在单个业务模块里用且很简单,直接在该模块里写 providers 可能更清晰。

register、forRoot、forFeature:它们是什么关系

先说结论:它们不是语法关键字,只是 Nest 生态里长期形成的命名约定,目的是让人一眼看懂“这个动态模块方法在语义上做什么”。

  • register(...):更通用的命名,表示“注册/配置一次模块”。常见于需要传入 options 的模块(例如 ClientsModule.register(...)JwtModule.register(...)MulterModule.register(...))。
  • forRoot(...):强调“根级别(应用级/全局级)配置”,通常只需要做一次,影响整个应用的默认行为或单例资源(例如 ConfigModule.forRoot(...)TypeOrmModule.forRoot(...))。
  • forFeature(...):强调“按功能域(feature)扩展/注册一小部分能力”,往往会被多次调用,每个业务模块各取所需(例如 TypeOrmModule.forFeature([Entity...])MongooseModule.forFeature([{ name, schema }...]))。

一个很好理解的类比是:

  • forRoot:建“基础设施”(连接、全局配置、默认客户端)
  • forFeature:在某个业务域里“挂载资源”(实体仓库、某些模型、某些订阅)
  • register:没有明确 root/feature 分层时的“通用注册入口”

使用方法(从 DynamicModule 结构看懂一切)

动态模块方法最终都要返回一个 DynamicModule 对象(官方文档的核心点之一)。你需要理解这些字段各自解决什么问题:

  • module:必须指向当前模块类本身(Nest 用它做标识与元数据合并)。
  • imports:该动态模块额外依赖的模块(例如需要先导入 ConfigModule 才能注入 ConfigService)。
  • providers:根据 options 生成/选择出来的 provider(通常包含 options provider、核心服务、工厂 provider 等)。
  • exports:允许外部模块使用的 provider(不导出就无法在外部注入)。
  • global(可选):设为 true 后,该模块导出的 provider 在整个应用可见(减少重复 imports,但要谨慎使用)。

下面用“伪代码”把三类方法串起来看。


register(...):通用注册入口

概念

register 通常用于:模块需要 options 才能工作,并且这个模块既可能全局用一次,也可能按需在少数地方导入,但作者不想强行区分 root/feature。

作用

  • 把调用方传入的 options 固化为一个可注入的 provider(常用做法是 useValue
  • 用这些 options 组装出真正的客户端/服务 provider(常用做法是 useFactory
  • 决定导出哪些 token 给外部模块使用

伪代码示例

// 伪代码:不依赖具体业务库,展示结构与思路
type FooModuleOptions = { baseUrl: string; timeoutMs?: number };

const FOO_OPTIONS = Symbol('FOO_OPTIONS');
const FOO_CLIENT = Symbol('FOO_CLIENT');

@Module({})
export class FooModule {
  static register(options: FooModuleOptions): DynamicModule {
    const optionsProvider = {
      provide: FOO_OPTIONS,
      useValue: { timeoutMs: 3000, ...options },
    };

    const clientProvider = {
      provide: FOO_CLIENT,
      useFactory: (opts: FooModuleOptions) => {
        // 这里可以 new 一个 HTTP client / SDK client
        return createFooClient(opts.baseUrl, opts.timeoutMs);
      },
      inject: [FOO_OPTIONS],
    };

    return {
      module: FooModule,
      providers: [optionsProvider, clientProvider],
      exports: [clientProvider],
    };
  }
}

何时使用

  • 你在写一个可复用模块:需要 options,但不想强制区分“全局/feature”两套 API。
  • 你希望调用方语义简单:FooModule.register({ ... }) 一眼看懂“我在配置这个模块”。

forRoot(...):应用级(根级别)初始化

概念

forRoot 的关键词是“根”。它通常承担两类职责:

  • 初始化一次:创建单例连接/单例客户端/全局默认配置。
  • 定义默认行为:例如全局中间件、全局拦截器/管道依赖的配置,或某个模块的“默认实例”。

在 Nest 的常见用法里:你会在 AppModule(或根模块)里调用 forRoot,其他业务模块不再重复调用,而是通过注入来使用它导出的 provider。

作用

  • 建立“全局共享的底座”:连接池、客户端单例、全局配置 provider 等。
  • 明确生命周期:避免每个 feature 模块都 new 一个连接或重复注册同一份全局配置。

伪代码示例

type FooRootOptions = { url: string };
const FOO_CONNECTION = Symbol('FOO_CONNECTION');

@Module({})
export class FooModule {
  static forRoot(options: FooRootOptions): DynamicModule {
    const connectionProvider = {
      provide: FOO_CONNECTION,
      useFactory: async () => {
        // 连接通常是 async 初始化
        return await connectFoo(options.url);
      },
    };

    return {
      module: FooModule,
      providers: [connectionProvider],
      exports: [connectionProvider],
      // 可选:如果你希望全局可见(谨慎)
      // global: true,
    };
  }
}

// AppModule 里只做一次 root 初始化
@Module({
  imports: [FooModule.forRoot({ url: '...' })],
})
export class AppModule {}

何时适合用 forRoot

  • 需要“只初始化一次”的资源:数据库连接、MQ 连接、缓存连接、全局配置加载等。
  • 你希望模块 API 语义明确:forRoot 让读代码的人直接知道“这是应用级初始化”。

forFeature(...):按业务域挂载/扩展能力

概念

forFeature 的关键词是“feature”。它解决的问题通常是:某个模块已经通过 forRoot 建好了底座,但不同业务模块只需要其中一部分资源,或者需要在该模块下再注册一批与业务相关的 provider。

经典例子(官方生态里最常见的理解方式):

  • ORM/ODM 模块在 root 初始化连接后,feature 模块再声明“我需要这些实体/模型”,框架据此生成仓库/模型 provider 并导出给当前业务模块使用。

作用

  • 把“业务域的声明”放在业务模块里:可读性强、边界清晰。
  • 支持多次调用:每个业务模块可以传不同的 feature 元数据。
  • 避免全量导出:只为当前 feature 生成它需要的 providers。

伪代码示例

type FooFeature = { name: string };
const fooFeatureToken = (name: string) => `FOO_FEATURE_${name}`;

@Module({})
export class FooModule {
  static forRoot(options: { url: string }): DynamicModule {
    // 省略:创建连接并导出
    return { module: FooModule, providers: [...], exports: [...] };
  }

  static forFeature(features: FooFeature[]): DynamicModule {
    const featureProviders = features.map((f) => ({
      provide: fooFeatureToken(f.name),
      useFactory: (conn: unknown) => {
        // conn 来自 forRoot 导出的连接 token
        return connCreateFeatureHandle(conn, f.name);
      },
      inject: [/* FOO_CONNECTION */],
    }));

    return {
      module: FooModule,
      providers: featureProviders,
      exports: featureProviders,
    };
  }
}

// 某个业务模块按需声明它要哪些 feature
@Module({
  imports: [FooModule.forFeature([{ name: 'User' }, { name: 'Order' }])],
})
export class UserDomainModule {}

何时适合用 forFeature

  • 你已经有一个“root 级底座”,但需要在不同业务模块里分别声明不同资源集合。
  • 你希望业务模块的依赖可读:打开模块文件就能看到它依赖了哪些实体/模型/功能片段。

三者在真实项目里的组合方式(推荐理解)

常见的组合模式是:

  • 基础设施模块XxxModule.forRoot(...)(只在根模块调用一次)
  • 业务域模块XxxModule.forFeature(...)(每个业务域各自声明所需)
  • 不分层或轻量模块XxxModule.register(...)(直接配置即可用)

如果你在某个三方库里同时看到 registerforRoot/forFeature

  • 通常意味着作者提供了多种入口,方便不同使用习惯;
  • 但底层本质仍然是返回 DynamicModule,差异更多在“语义分层”和“推荐调用位置”。

注意事项(容易踩坑但官方语义允许你避免)

  • 不要把 forRoot 到处调用:如果它创建的是连接/单例资源,多次调用往往意味着多份实例(开销大、难排查)。更稳妥的模式是 root 初始化一次,feature 按需挂载。
  • 导出策略要克制exports 只导出真正需要给外部用的 provider。导出太多会让依赖边界变模糊,也会增加误用概率。
  • token 设计要稳定:options provider、客户端 provider、feature provider 的 token 一旦对外暴露,后续变更会影响大量模块。推荐用常量/Symbol/统一工厂函数生成 token,避免字符串散落。
  • global: true 谨慎使用:全局模块能减少 imports,但也会让依赖变“隐式”。团队协作里,显式 imports 往往更可维护。
  • 考虑异步配置:如果 options 依赖配置中心/远程拉取/ConfigService,一般会需要 registerAsync / forRootAsync 这一类异步变体(很多官方生态模块也提供同名 Async 方法)。

小结

动态模块的本质是:用一个静态方法返回 DynamicModule,把“模块如何被配置、生成哪些 provider、导出哪些能力”收敛为一个清晰入口register / forRoot / forFeature 是社区约定的命名语义:

  • forRoot:做应用级初始化(通常一次),建立底座与默认能力
  • forFeature:按业务域扩展/声明所需资源(可多次),只生成当前 feature 需要的 providers
  • register:通用注册入口(语义不分层),把 options 转成可注入能力即可用

掌握这三者,你读三方模块源码时会更快看懂“哪里初始化一次、哪里按需扩展、哪些 provider 会被导出”,写自己的可复用模块时也能把配置与依赖边界做得更清楚。

Nestjs 中 Provider 的注入方式扫盲,解决你的选择困难症

Providers 是 Nest 中的一个核心概念。许多基础的 Nest 类,如服务、仓库、工厂和辅助工具,都可以被视为提供者。提供者的核心思想是它可以作为依赖被注入,从而允许对象之间形成各种关系。“连接”这些对象的责任在很大程度上由 Nest 运行时系统处理。

但是 Providers 的注入方式有很多种,对此不了解的同学在开发中遇到时,可能难以选择该用哪一种方式,这篇文章就针对这一点做一个详细的阐述


0. 先把话说清楚:你纠结的其实是两件事

在 Nest 里,“我想用一个 Provider”通常包含两步:

  • 注册(registration):把某个 token 和“怎么得到这个值/实例”的规则,交给 Nest 的 IoC 容器管理(通常写在 @Module({ providers: [...] }) 里)。
  • 注入(injection):在需要它的地方声明依赖,让 Nest 在创建类实例时把它“塞进来”(最常见是构造函数注入)。

另外要记住一个关键词:token

  • 最常用的 token:类本身(例如 CatsService)。
  • 也可以用:字符串、Symbol、TypeScript enum(官方明确提到可以用这些)。
  • 不建议/不能直接用:TypeScript interface(运行时不存在,容器没法拿它当 token 匹配)。

接下来所有“方式”,本质都是围绕 token 在做文章:要么变更“这个 token 对应哪个实现/值”,要么变更“这个实例的创建时机与生命周期”。


1. 方式一(默认首选):按类名 token 的构造器注入(Standard provider)

何时使用

  • 绝大多数业务场景的默认选择:Service / Repository / Helper 这类“可复用、可测试”的逻辑单元。
  • 当你不需要动态切换实现、不需要注入常量/第三方实例时,用它最省心。

典型场景

  • Controller 调 Service,Service 调 Repository。
  • 业务逻辑都在 class 里,依赖关系清晰。

注意事项

  • 别忘了注册:类写了 @Injectable() 只是“允许被容器管理”,但你仍要把它放进某个模块的 providers(或被某个模块导入后可见)。
  • 跨模块要 export/import:Provider 默认只在声明它的模块内部可见;要给别的模块用,需要 exports

伪代码

// cats.module.ts
@Module({
  providers: [CatsService], // 这是最常见的“短写”
  exports: [CatsService],   // 需要给别的模块用就导出
})
class CatsModule {}

// cats.controller.ts
@Controller('cats')
class CatsController {
  constructor(private readonly catsService: CatsService) {}
}

小知识:providers: [CatsService] 其实是下面这种“长写”的语法糖:{ provide: CatsService, useClass: CatsService }。理解这个等价关系,会让你更容易看懂后面的自定义 Provider。


2. 方式二:自定义 token + @Inject(token) 注入(字符串 / Symbol / enum)

何时使用

  • 你要注入的东西不是一个 class:常量、配置对象、第三方库实例(DB 连接、Redis client、SDK)。
  • 你想用一个“抽象 token”来隔离实现:例如用 CONFIG/CONNECTION 这类 token,让依赖方不直接 import 具体实现文件。

典型场景

  • 数据库连接、消息队列 client、第三方 SDK 实例。
  • 为了避免“魔法字符串”到处飘,集中管理 token。

注意事项

  • 尽量别直接散落字符串 token:官方建议把 token 放到独立文件(如 constants.ts)统一导出,避免冲突和拼写错误。
  • 更推荐 Symbol:字符串容易撞名;Symbol('CONNECTION') 更不容易冲突。

伪代码

// constants.ts
export const CONNECTION = Symbol('CONNECTION');

// db.module.ts
@Module({
  providers: [
    { provide: CONNECTION, useValue: connectionInstance },
  ],
  exports: [CONNECTION],
})
class DbModule {}

// cats.repository.ts
@Injectable()
class CatsRepository {
  constructor(@Inject(CONNECTION) private readonly conn: Connection) {}
}

3. 方式三:useValue(值提供者 Value provider)

何时使用

  • 注入常量值、配置对象、已经创建好的实例。
  • 测试/本地调试时,用 mock 替换真实实现(官方也拿它举例)。

典型场景

  • useValue: mockService 做单元测试替身。
  • 注入某个第三方库的“现成对象”(例如 logger、连接句柄)。

注意事项

  • useValue 直接把一个值交给容器:不会由 Nest new,也不会帮你管理它的内部依赖。
  • 如果你用它替换一个 class provider,确保这个值的“形状”能满足调用方需要(在 TS 里通常靠结构化类型兼容)。

伪代码

const mockCatsService = { findAll: () => [] };

@Module({
  providers: [
    { provide: CatsService, useValue: mockCatsService },
  ],
})
class TestModule {}

4. 方式四:useClass(类提供者 Class provider)

何时使用

  • 你想让一个 token 在不同环境/条件下解析到不同的实现类
  • 例如开发环境用 DevConfigService,生产环境用 ProdConfigService

典型场景

  • 多套实现按环境切换(dev/prod)。
  • 同一抽象能力的多实现(例如不同供应商的短信服务)。

注意事项

  • 依赖方注入的是 token(通常是一个“抽象入口”),不要在依赖方写 if/else 去挑实现,把选择逻辑放在 provider 注册处。

伪代码

const configProvider = {
  provide: ConfigService,
  useClass: isDev ? DevConfigService : ProdConfigService,
};

@Module({ providers: [configProvider] })
class AppModule {}

5. 方式五:useFactory(工厂提供者 Factory provider)

何时使用

  • 你需要“动态创建”一个实例:创建过程要读配置、组合参数、甚至依赖别的 Provider。
  • 你需要“异步初始化”后才允许系统启动(比如先连上数据库再接请求)。

典型场景

  • DB 连接创建、缓存 client 创建、按配置生成 SDK 实例。
  • 一部分依赖可选:没有就用默认行为。

注意事项

  • inject 数组的顺序要和工厂函数参数一一对应(官方明确说明会按顺序传参)。
  • inject 里可以声明可选依赖:{ token: XXX, optional: true },工厂函数就要能处理 undefined
  • 异步 provider:工厂返回 Promise 时,Nest 会等待它 resolve 后,才会实例化依赖它的类(官方在“Async providers”章节强调这一点)。

伪代码(同步 + 可选依赖)

const connectionProvider = {
  provide: CONNECTION,
  useFactory: (options: OptionsProvider, maybePrefix?: string) => {
    const opts = options.get();
    return new DatabaseConnection({ ...opts, prefix: maybePrefix });
  },
  inject: [
    OptionsProvider,
    { token: 'SOME_OPTIONAL', optional: true },
  ],
};

伪代码(异步初始化)

const asyncConnectionProvider = {
  provide: 'ASYNC_CONNECTION',
  useFactory: async () => {
    const conn = await createConnection(options);
    return conn;
  },
};

注入时和普通 provider 一样,只是 token 不同:

constructor(@Inject('ASYNC_CONNECTION') conn: Connection) {}

6. 方式六:useExisting(别名提供者 Alias provider)

何时使用

  • 你想让两个 token 指向同一个 Provider 实例(官方称之为 alias)。
  • 常见于迁移期:旧代码用旧 token,新代码用新 token,但底层实现先共用一份。

典型场景

  • 日志服务从 LoggerService 迁到 'LOGGER',但一段时间内两种写法都得兼容。

注意事项

  • useExisting 不是创建新实例,而是“多一个入口指向同一个实例”。
  • 在默认单例(DEFAULT)下,两边拿到的是同一对象;如果你用了请求级/瞬态作用域,要更小心理解生命周期(见后文“作用域”)。

伪代码

const loggerAliasProvider = {
  provide: 'AliasedLoggerService',
  useExisting: LoggerService,
};

@Module({ providers: [LoggerService, loggerAliasProvider] })
class AppModule {}

7. 跨模块使用:导出(export)自定义 Provider

何时使用

  • 你的 Provider 定义在 DbModuleConfigModule 里,但别的模块要注入它。

典型场景

  • DbModule 里创建连接 provider,在 UserModule / OrderModule 注入使用。

注意事项

  • 自定义 Provider 默认只在本模块可见,要给别人用必须导出。
  • 官方给了两种导出方式:
    • exports: [TOKEN](导出 token)
    • exports: [providerObject](导出整个 provider 定义)

伪代码

const connectionFactory = { provide: 'CONNECTION', useFactory: ..., inject: [...] };

@Module({
  providers: [connectionFactory],
  exports: ['CONNECTION'], // 或 exports: [connectionFactory]
})
class DbModule {}

8. “可选依赖”到底怎么写?

官方文档里最直接、最可控的一种可选依赖写法,是在 useFactoryinject 里声明 optional: true

inject: [MyOptionsProvider, { token: 'SomeOptionalProvider', optional: true }]

这会让工厂函数对应参数可能为 undefined使用场景通常是“有则增强、无则降级”的依赖,比如可选的前缀、可选的扩展配置、可选的监控上报器等。

注意事项很朴素:你必须把 undefined 当成合法输入处理掉,否则等同于把问题从“容器解析阶段”推迟到“运行时崩溃阶段”。


9. 属性注入(Property-based injection)要不要用?

Nest 支持用 @Inject(token) 在属性上注入,但官方长期更强调构造器注入这条主路径。实际工程里,一般建议把属性注入当成“应急方案”:

何时使用

  • 你在做一些元编程/基类封装,构造器签名不方便改动。
  • 你非常明确这不会让依赖关系变得隐蔽(例如只在框架层封装里用)。

不太建议的原因

  • 依赖不在构造器里显式声明,阅读类定义时更难一眼看出“需要哪些东西”。
  • 测试替换与重构成本更高,容易留下隐性依赖。

伪代码

@Injectable()
class CatsRepository {
  @Inject(CONNECTION)
  private readonly conn: Connection;
}

10. 循环依赖:forwardRef()ModuleRef 的取舍

循环依赖指 A 依赖 B、B 也依赖 A。Nest 官方给了两条路:

方式 A:forwardRef()(最常用)

何时使用

  • 两个 Provider 真的是互相需要,而且短期内不好拆。

注意事项(官方强调的坑)

  • 实例化顺序不确定,代码不要依赖“谁先构造”。
  • 如果循环依赖链上出现 Scope.REQUEST 的 provider,可能导致依赖变成 undefined(官方给了明确 warning)。
  • 还有一种“看似 DI 的循环依赖”,其实是 barrel file(index.ts 聚合导出)导致的 import 循环;官方建议在模块/Provider 类上尽量避免 barrel file。

伪代码:

@Injectable()
class AService {
  constructor(@Inject(forwardRef(() => BService)) private b: BService) {}
}

@Injectable()
class BService {
  constructor(@Inject(forwardRef(() => AService)) private a: AService) {}
}

模块之间循环 import 也同理:

@Module({ imports: [forwardRef(() => BModule)] })
class AModule {}

@Module({ imports: [forwardRef(() => AModule)] })
class BModule {}

方式 B:ModuleRef(重构友好)

何时使用

  • 你想把循环依赖“断开一边”,让其中一方在运行时按需从容器取实例(而不是在构造器里硬绑死)。

注意事项

  • 这通常意味着你在改设计:把“必须在构造器里就拿到依赖”变成“需要时再取”,要保证调用路径上能接受这种变化。

11. 作用域(Injection scopes):默认单例、请求级、瞬态

Nest 官方把 Provider 生命周期分为三类:

  • DEFAULT(默认):全局单例,应用生命周期内共享一份实例。官方也明确说:大多数场景推荐单例
  • REQUEST:每个请求一份实例,请求结束后释放。适合“按请求隔离状态”的边界场景。
  • TRANSIENT:每个注入点(每个消费者)都会拿到一份新实例。

何时使用 REQUEST

  • GraphQL 的按请求缓存、请求链路追踪、多租户(根据请求头选择租户上下文)等官方列出的典型例子。

注意事项

  • 性能影响:请求级 provider 会让 DI 子树在每个请求都创建实例,官方建议除非必须,否则优先单例。
  • 作用域会沿依赖链“向上冒泡”:Controller 依赖了 request-scoped provider,那么 Controller 自己也会变成 request-scoped。
  • WebSocket Gateway 不应使用 request-scoped:官方明确指出它们必须是单例;Passport strategy、Cron 等也有类似限制。

伪代码

@Injectable({ scope: Scope.REQUEST })
class RequestCacheService {}

// 或者在自定义 provider 上设置 scope
{ provide: 'CACHE_MANAGER', useClass: CacheManager, scope: Scope.TRANSIENT }

小结

  • 能用构造器注入 + 类 token 就别复杂化providers: [MyService] + constructor(private my: MyService) 是默认正确答案。
  • 要注入“不是 class 的东西”:用自定义 token(优先 Symbol)+ @Inject(token)
  • 要替换实现 / mock / 常量useValue
  • 要按环境/条件切换实现useClass
  • 要动态创建/组合依赖/异步初始化useFactory(需要 async 就直接返回 Promise)。
  • 要做兼容/迁移/多入口同实例useExisting
  • 遇到循环依赖:优先重构拆分;确实拆不开再用 forwardRef(),并避开 request-scoped 组合的坑。
  • 作用域:默认单例最香;REQUEST/TRANSIENT 是为边界问题准备的“手术刀”,别当“菜刀”乱用。

参考(官方文档)

一次讲透 NestJS 里“绑定”(全局 vs 局部)

你在 NestJS 里看到的 @UseGuards()@UsePipes()app.useGlobalInterceptors() 这些,本质上都在做一件事:

  • 把一段“横切逻辑”挂到请求处理链上
    比如:鉴权、参数校验、日志、统一返回体、统一异常格式……

这篇就用“人话”把三个问题讲清楚:

  • NestJS 里可绑定的【元素】有哪些
  • 全局绑定 vs 局部绑定:作用与区别
  • 全局绑定的多种形式:各自原理/传参/差异/注意点,以及怎么选

本文所有结论都以 NestJS 官方文档为准(会在对应小节标注链接)。

目录

  1. NestJS 里能“绑定”的【元素】有哪些?
  2. 全局绑定 vs 局部绑定:作用与区别
  3. 全局绑定的多种形式:到底差在哪?
  4. 五类元素分别怎么绑、怎么传参、有哪些坑?
  5. 选型:什么时候用哪种绑定方式?
  6. 总结

1. NestJS 里能“绑定”的【元素】有哪些?

日常开发里,最常说的“绑定”,基本就这五类(也是官方文档重点讲的五条链路):

  • Middleware(中间件):在路由处理前跑的一段函数/类,能拿到 req/res/next
    参考:Middleware
  • Guard(守卫):决定“这次请求到底能不能进到 handler”。典型用来做鉴权/权限。
    参考:Guards
  • Pipe(管道):对入参做校验转换(字符串转数字、DTO 校验等),发生在方法调用前。
    参考:Pipes
  • Interceptor(拦截器):更像 AOP,能在 handler 前后插逻辑、改返回值、做缓存、把异常映射成别的异常等。
    参考:Interceptors
  • Exception Filter(异常过滤器):专门兜异常,统一格式、打日志、屏蔽敏感信息等。
    参考:Exception filters

如果你要一个“背诵版”的链路顺序,官方明确写过的一句是:

  • Guard 在所有 Middleware 之后执行,并且在任何 Interceptor 或 Pipe 之前执行
    参考:Guards - Hint

2. 全局绑定 vs 局部绑定:作用与区别(别背概念,直接按场景理解)

先把“范围”说清楚,后面选型才不容易绕晕。

  • 局部绑定(Local / Scoped):只影响“某个控制器 / 某个路由方法 / 某个参数”。
    典型写法:@UseGuards()@UsePipes()@UseInterceptors()@UseFilters(),以及 Pipe 还能绑到参数上。
  • 全局绑定(Global):影响“整个应用里所有 controller + 所有 route handler”。
    典型写法:app.useGlobalXxx(...)、模块 providers 里用 APP_XXX、Middleware 的 app.use(...) / forRoutes('*')

一句话区分:

  • 局部绑定:像“给某个接口/模块单独加一条规则”
  • 全局绑定:像“把规则写进公司制度,所有人默认都得遵守”

3. 全局绑定不止一种写法:到底差在哪?

这个是很多人纠结的核心:为什么全局还能写出两三种形式?我该用哪个?

3.1 main.tsapp.useGlobalXxx(new ...):直给、简单,但 DI 有坑

以 Pipe 为例,官方给过最直观的全局写法:

// main.ts
const app = await NestFactory.create(AppModule);
app.useGlobalPipes(new ValidationPipe());

参考:Pipes - Global scoped pipes

这种写法的本质是:你自己把实例 new 出来,挂到应用上

还有个容易被忽略的“覆盖范围”问题:在混合应用(HTTP + WS/微服务)里,useGlobalPipes() / useGlobalGuards() 默认不一定覆盖网关/微服务。官方在 pipes/guards 里都有提醒。
参考:

3.2 模块里用 APP_PIPE / APP_GUARD / APP_INTERCEPTOR / APP_FILTER:更“框架化”,DI 友好

官方给的“解决 DI 问题”的标准姿势,就是把它注册成 provider:

// app.module.ts(示例:全局 Pipe)
import { APP_PIPE } from '@nestjs/core';

@Module({
  providers: [
    { provide: APP_PIPE, useClass: ValidationPipe },
  ],
})
export class AppModule {}

参考:Pipes - Global scoped pipes(APP_PIPE)

Guard / Interceptor / Filter 的写法完全一样,只是 token 变了:

这种写法的本质是:交给 Nest DI 容器来创建实例

  • 优点:能注入依赖;更容易做可测试的设计;在复杂业务里更推荐
  • 注意:官方也强调——不管你在哪个 module 里写,它都是“真的全局”,建议放在“该类定义所在的 module”
    参考同上各章的 Hint(都提到了“choose the module where X is defined”)

3.3 装饰器里传“类” vs 传“new 出来的实例”:你其实是在选“谁来创建对象”

官方在多个章节都写过:装饰器里你可以传,也可以传实例

以 Guard 为例:

@UseGuards(RolesGuard)       // 传类:Nest 来实例化,可 DI
@UseGuards(new RolesGuard()) // 传实例:你来实例化,一般就别指望 DI 了

参考:Guards - Binding guards

Pipe/Interceptor/Filter 也是同理(官方都写了“pass class enables dependency injection / pass in-place instance for customization”那套逻辑)。

简单粗暴的结论:

  • 想要 DI:尽量传类(或用 APP_XXX
  • 想要按接口定制参数(比如某个 ParseIntPipe 想改 errorHttpStatusCode):就传 new Xxx(options)

3.4 Middleware 的全局绑定更“特别”:app.use() 很香,但它根本进不了 DI

官方对 middleware 的说明更直白:

  • app.use(logger) 能一次绑到所有路由,但无法访问 DI 容器
    参考:Middleware - Global middleware
  • 如果你需要 DI,就别用 app.use();改用 class middleware + .forRoutes('*')(它运行在 module 里,能注入)
    参考同上(官方也给了替代方案)

4. 逐个元素讲清楚:怎么绑、怎么传参、有哪些坑

下面每个元素我都给你:能绑在哪些层级 + 全局的几种写法 + 需要注意的点 + 伪代码

4.1 Middleware(中间件)

能绑在哪些层级

  • 模块/路由级consumer.apply(...).forRoutes(...)(最常用)
  • 全局app.use(...)(但不走 DI)

绑定伪代码

// 1) 模块内绑定(推荐:可 DI)
export class AppModule implements NestModule {
  configure(consumer: MiddlewareConsumer) {
    consumer.apply(LoggerMiddleware).forRoutes('cats'); // 只绑 /cats

    consumer
      .apply(LoggerMiddleware)
      .forRoutes({ path: 'cats', method: RequestMethod.GET }); // 只绑 GET /cats

    consumer
      .apply(LoggerMiddleware)
      .exclude(
        { path: 'cats', method: RequestMethod.GET },
        'cats/{*splat}',
      )
      .forRoutes(CatsController); // 除了排除的,其他都绑
  }
}

// 2) 全局绑定(简单,但 DI 不可用)
const app = await NestFactory.create(AppModule);
app.use(logger); // logger 是 functional middleware

参考:Middleware - Applying middleware / Excluding routes / Global middleware

特别注意

  • 不调用 next() 请求会挂住(官方原话就是“request will be left hanging”)
    参考:Middleware
  • app.use() 的全局 middleware 拿不到 DI(要 DI 就用 .forRoutes('*') 那套)
    参考:Middleware - Global middleware
  • Express 与 Fastify 的 middleware 签名不一样(官方有 warning)
    参考同上:Middleware - Warning

4.2 Guard(守卫)

能绑在哪些层级

  • Controller 级@UseGuards() 写在类上
  • Method 级@UseGuards() 写在方法上
  • 全局app.useGlobalGuards(...)APP_GUARD

绑定伪代码

// 局部:controller 级
@Controller('cats')
@UseGuards(RolesGuard)
export class CatsController {}

// 局部:method 级
@Post()
@UseGuards(RolesGuard)
create() {}

// 全局:main.ts(不走 DI)
const app = await NestFactory.create(AppModule);
app.useGlobalGuards(new RolesGuard());

// 全局:APP_GUARD(走 DI,推荐)
@Module({
  providers: [{ provide: APP_GUARD, useClass: RolesGuard }],
})
export class AppModule {}

参考:Guards - Binding guards

特别注意

  • 执行顺序:Guard 在 middleware 之后,在 interceptor/pipe 之前
    参考:Guards - Hint
  • 混合应用覆盖范围useGlobalGuards() 在 hybrid app 默认不覆盖网关/微服务(官方 Notice)
    参考:Guards - Binding guards
  • 全局 + DI:要 DI 就别在 main.tsnew,用 APP_GUARD
    参考同上(官方写得很明确)

4.3 Pipe(管道)

Pipe 这块“绑的层级”最多,也是最容易写出花的。

能绑在哪些层级

  • 参数级@Param('id', ParseIntPipe) / @Body(new ValidationPipe())
  • 方法级@UsePipes(...)
  • Controller 级@UsePipes(...) 写在类上
  • 全局useGlobalPipes()APP_PIPE

绑定伪代码

// 参数级:把 id 转成 number,不行就直接 400
@Get(':id')
findOne(@Param('id', ParseIntPipe) id: number) {}

// 参数级:定制 options,就 new 一个实例
@Get(':id')
findOne(
  @Param('id', new ParseIntPipe({ errorHttpStatusCode: HttpStatus.NOT_ACCEPTABLE }))
  id: number,
) {}

// 方法级:按接口传 schema(典型“每个接口一套校验规则”)
@Post()
@UsePipes(new ZodValidationPipe(createCatSchema))
create(@Body() dto: CreateCatDto) {}

// 全局:main.ts(不走 DI)
const app = await NestFactory.create(AppModule);
app.useGlobalPipes(new ValidationPipe());

// 全局:APP_PIPE(走 DI,推荐)
@Module({
  providers: [{ provide: APP_PIPE, useClass: ValidationPipe }],
})
export class AppModule {}

参考:Pipes - Binding pipes / Global scoped pipes

特别注意

  • Pipe 抛异常会进入异常层处理(官方叫 exceptions zone),抛了异常 handler 就不会执行
    参考:Pipes - Hint
  • 混合应用覆盖范围useGlobalPipes() 在 hybrid app 下默认不覆盖网关/微服务(官方 Notice)
    参考:Pipes - Global scoped pipes
  • 全局 + DI:同样要用 APP_PIPE(官方直接点名)
    参考同上

4.4 Interceptor(拦截器)

能绑在哪些层级

  • Controller / Method 级@UseInterceptors(...)
  • 全局useGlobalInterceptors()APP_INTERCEPTOR

绑定伪代码

// 局部:controller 级
@UseInterceptors(LoggingInterceptor)
export class CatsController {}

// 全局:main.ts(不走 DI)
const app = await NestFactory.create(AppModule);
app.useGlobalInterceptors(new LoggingInterceptor());

// 全局:APP_INTERCEPTOR(走 DI,推荐)
@Module({
  providers: [{ provide: APP_INTERCEPTOR, useClass: LoggingInterceptor }],
})
export class AppModule {}

参考:Interceptors - Binding interceptors

特别注意

4.5 Exception Filter(异常过滤器)

能绑在哪些层级

  • Method / Controller 级@UseFilters(...)
  • 全局useGlobalFilters()APP_FILTER

绑定伪代码

// 局部:method 级
@Post()
@UseFilters(HttpExceptionFilter) // 推荐传类,让 Nest 复用实例
create() {}

// 全局:main.ts(不走 DI)
const app = await NestFactory.create(AppModule);
app.useGlobalFilters(new HttpExceptionFilter());

// 全局:APP_FILTER(走 DI,推荐)
@Module({
  providers: [{ provide: APP_FILTER, useClass: HttpExceptionFilter }],
})
export class AppModule {}

参考:Exception filters - Binding filters

特别注意


5. 到底怎么选?给你一套“能直接落地”的决策规则

你可以按这几个问题来选,基本不踩坑。

5.1 这段逻辑是不是“所有接口都必须有”?

  • (比如统一校验/统一返回体/统一异常格式/全局鉴权):倾向全局
  • 不是(只对某几个接口/某个模块生效):局部绑定,别污染全局

5.2 这段逻辑要不要注入 Service / Config / DB / Cache?

  • 要 DI
    • Guard/Pipe/Interceptor/Filter:优先用 APP_GUARD / APP_PIPE / APP_INTERCEPTOR / APP_FILTER
    • Middleware:优先用 class middleware + consumer.apply(...).forRoutes('*')
  • 不要 DI
    • 你就可以用 main.tsuseGlobalXxx(new ...)app.use(...),写起来最快

(这些 DI 限制和替代方案,官方都写在对应章节里了:
PipesGuardsInterceptorsException filtersMiddleware

5.3 你需要“每个接口参数不一样”吗?

  • 需要(比如某个 Pipe 要带不同 options、或者每个接口校验 schema 不一样):局部 new Xxx(options) 更合适
  • 不需要(全站统一同一套配置):全局注册一次,别每个方法都写一遍

5.4 你项目是不是 hybrid(HTTP + WS/微服务)?

如果是,别默认以为 useGlobalXxx() 就全覆盖。官方在 pipes/guards/filters 里都写了“hybrid app”的注意点,建议你在项目里明确验证一下覆盖范围:


6. 总结

  • 能绑定的核心元素就 5 个:Middleware、Guard、Pipe、Interceptor、Exception Filter。
  • 局部绑定解决“精准控制”:只对某个 controller / method / param 生效,最不容易“误伤”别的模块。
  • 全局绑定解决“统一规则”:所有接口默认生效,但你要对“DI 能不能用、hybrid 覆盖范围”保持敏感。
  • 全局绑定最重要的分水岭是 DI
    • main.tsuseGlobalXxx(new ...):快,但基本不走 DI
    • module 里的 APP_XXX:更工程化,DI 友好,复杂项目更推荐
    • middleware 的 app.use():最简单,但拿不到 DI;要 DI 就 .forRoutes('*')
  • 装饰器传“类”还是“实例”:你其实是在决定“让 Nest 创建对象(可 DI、可复用)”还是“自己 new(方便定制参数)”。

一文讲清 NestJS 中 IoC、DI、AOP、DTO、Entity 等名词

上一篇《构建一个 NestJS 应用程序需要具备哪些基础元素?》里,我们把 Module / Controller / Provider / Guard / Pipe… 这些“组件角色”捋了一遍。

但很多人(包括我一开始)真正卡住的,其实是另一层:这些角色里面提到的名词到底表示啥?

这篇就专门把 NestJS 里常见的英文缩写/名词一次性讲清楚

先给一张速查表(用来对号入座)

缩写/名词 中文名 一句话记住 在 NestJS 里常落到哪
IoC (Inversion of Control) 控制反转 “对象怎么创建/怎么组合”交给框架 @nestjs/core 的容器/加载流程
DI (Dependency Injection) 依赖注入 “依赖不要自己 new,框架帮你注入” Provider 构造函数注入、@Inject()
AOP (Aspect-Oriented Programming) 面向切面编程 把日志/鉴权/校验这类横切逻辑“抽出去” Middleware / Guard / Pipe / Interceptor / Filter
DTO (Data Transfer Object) 数据传输对象 传输用的数据结构(尤其是入参) @Body() + DTO class + ValidationPipe
Entity 实体(实体类) 持久化模型(数据库表/集合的映射) TypeORM/Prisma/Mongoose 各自的实体/模型
ORM 对象关系映射 “对象 ↔ 表”映射(更广义:把数据库访问抽象成模型/接口) TypeORM / Prisma(Prisma 更像 ORM-like 的类型安全 DB Client)
ODM 对象文档映射 “对象 ↔ 文档”映射(Mongo) Mongoose
CRUD 增删改查 最常见的接口形态(创建/查询/更新/删除) Controller + Service + Repository
REST 表述性状态转移(常说 RESTful) 用“资源 + HTTP 方法”来组织 API Controller 的路由设计
HTTP 超文本传输协议 Web 接口最常见的传输方式 @Controller() / @Get() / @Post()
RPC 远程过程调用 更像“调用方法”,常见于服务间通信 @nestjs/microservices
RxJS 响应式扩展 响应式编程库(NestJS 部分链路会用到) Interceptor 里的 pipe(map(...))
Observable 可观察对象/流 RxJS 的核心类型(可订阅的异步流) CallHandler.handle() 返回值
JWT JSON Web Token(JSON Web 令牌) 常见的无状态 token 方案 Guard + Passport Strategy
RBAC 基于角色的访问控制 “角色 → 权限”的经典权限模型 Guard(配合装饰器/元数据)
CQRS 命令查询职责分离 复杂业务下的读写分离组织方式 @nestjs/cqrs(可选,不是必需品)
CLI 命令行接口(脚手架) 生成模板代码、统一项目结构 @nestjs/cli
CORS 跨域资源共享 浏览器跨域访问控制 app.enableCors()
CSRF 跨站请求伪造 利用 Cookie 自动携带发起伪造请求 结合鉴权方式 + 中间件/策略设计

IoC:Inversion of Control(控制反转)

是什么

在软件工程里,IoC 指把“控制权”(对象创建、生命周期管理、依赖装配、调用时机等)从业务代码转移给框架/容器来统一管理。
它被提出的目的,是降低耦合集中扩展点、让项目变大后依赖关系仍然可控(而不是到处 new、到处传参)。

一句话:对象的创建与组装不由你手写流程控制,而由框架在启动时统一完成。

适合干啥

  • 让项目变大后还能“有秩序地装配依赖”(否则满世界 import/new/传参)
  • 让模块边界更清晰:你用“声明”代替“到处调用”

何时使用

在 NestJS 里你基本“自动就在用 IoC”了,因为 NestJS 本身就是基于容器的框架。你需要做的更多是:别破坏它(比如绕过容器到处 new)。

原理(抓重点就行)

  • 启动时,NestJS 会扫描模块(Module)、收集 Provider、构建依赖图(谁依赖谁)
  • 需要实例化某个类时,由容器按依赖图“自底向上”创建并缓存(默认单例)

伪代码示例

// 你只声明“我需要什么”
@Injectable()
class UserService {
  constructor(private repo: UserRepo) {}
}

// 你只声明“这个模块包含哪些”
@Module({ providers: [UserRepo, UserService] })
class UserModule {}

// 剩下的“怎么创建 repo 再创建 service”,框架搞定(IoC)

DI:Dependency Injection(依赖注入)

是什么

DI 是 IoC 的一种常见实现方式,它强调“依赖从外部注入”,而不是在类内部主动创建依赖。
目的很直接:让代码更解耦、依赖更可替换(方便测试/替身实现)、也更容易在大型项目里统一管理。

一句话:把“依赖关系”从代码里的 new,变成声明式注入。

适合干啥

  • 让 service 更容易测试(可以替换依赖、注入 mock)
  • 避免强耦合(service 不需要知道 repo 如何创建)

何时使用

你写 NestJS 业务时,绝大多数依赖都推荐走 DI:

  • service 依赖 repo / http client / config / logger
  • guard 依赖 auth service
  • interceptor 依赖 cache service

原理

DI 的核心是“token → provider”的映射:

  • 最常见 token 就是 class 本身(UserService
  • 容器根据构造函数参数的类型/注入 token,找到对应 provider,注入实例

伪代码示例

@Injectable()
class UserService {
  constructor(private readonly repo: UserRepo) {}
}

// 自定义 token(比如注入第三方库实例)
const REDIS = Symbol('REDIS');

@Module({
  providers: [
    { provide: REDIS, useValue: /* redisClient */ {} },
    { provide: UserRepo, useClass: UserRepo },
  ],
})
class InfraModule {}

@Injectable()
class CacheService {
  constructor(@Inject(REDIS) private redis: any) {}
}

AOP:Aspect-Oriented Programming(面向切面)

是什么

AOP 是一种把“横切关注点”(logging、auth、validation、metrics 等)从核心业务逻辑中分离出来的思想,常见手段是拦截/代理,在函数执行前后“织入”通用逻辑。
提出它的目的,是减少重复代码、统一策略,让业务代码更专注在“做业务”。

一句话:把“到处都要做”的横切逻辑(日志/鉴权/校验/统一返回)从业务代码里抽出来。

适合干啥

  • 统一做日志、耗时统计、异常格式、权限校验
  • 让 controller/service 更“干净”,专注业务

何时使用(别上来就 AOP 过度)

当你发现同一段逻辑在 N 个接口里重复出现时,AOP 才开始值钱。
如果只是一个接口的特殊处理,直接写在 handler 里往往更直观。

原理(用 NestJS 的话来讲)

NestJS 不是“只有一种 AOP”,它把 AOP 拆成几类工具,各司其职:

  • Middleware:请求刚进门(路由前)
  • Guard:能不能进(鉴权/权限)
  • Pipe:入参校验/转换
  • Interceptor:前后包一层(统一返回/缓存/耗时)
  • Exception Filter:统一错误输出

它们共同点是:不改业务函数签名,也能在请求链路上插入逻辑。

伪代码示例

// 统一返回结构:{ code, data, traceId }
@Injectable()
class WrapResponseInterceptor implements NestInterceptor {
  intercept(ctx, next) {
    const traceId = /* get from request */ 'xxx';
    return next.handle().pipe(map(data => ({ code: 0, data, traceId })));
  }
}

// 鉴权:没 token 就不让进
@Injectable()
class AuthGuard implements CanActivate {
  canActivate(ctx) {
    const req = ctx.switchToHttp().getRequest();
    return Boolean(req.headers.authorization);
  }
}

DTO:Data Transfer Object(数据传输对象)

是什么

DTO 常用于分层架构/分布式系统的“边界处”,用来描述数据在层与层(或服务与服务)之间传输的结构。它强调“只承载数据”,不强调业务行为。
提出它的目的,是把外部输入/输出与内部模型隔离开:API 契约清晰不把内部 Entity/领域对象直接暴露出去

一句话:专门用来“接收/传输”的数据结构(尤其是“请求入参”)。

适合干啥

  • 把“接口入参长啥样”讲清楚(更容易维护)
  • 配合校验:让脏数据在进业务之前就被拦住

何时使用

只要是对外接口(HTTP/RPC/GraphQL)基本都建议用 DTO:

  • CreateUserDtoUpdateUserDtoQueryUserDto

原理(别纠结细节,抓住链路)

典型链路是:@Body() 拿到原始对象 → ValidationPipe 校验/转换 → 传入 controller 方法参数。
常见组合是 class-validator + class-transformer(你会在项目里看到它们)。

伪代码示例

class CreateUserDto {
  // @IsEmail()
  email: string;

  // @MinLength(8)
  password: string;
}

@Controller('users')
class UserController {
  @Post()
  create(@Body() dto: CreateUserDto) {
    return this.userService.create(dto);
  }
}

// main.ts 里常见开启方式
app.useGlobalPipes(new ValidationPipe({ whitelist: true, transform: true }));

Entity:实体(通常指持久化实体)

是什么

在不同语境里 Entity 有两层含义:

  • 在 DDD 里,Entity 是“有持续身份(identity)的对象”,即便属性变了,它仍然是同一个实体
  • 在 ORM 里,Entity 往往指“表/集合的映射模型”(实体类/模型定义)
    提出它的目的,是用更结构化的方式表达持久化数据(字段/关系/约束),让数据访问与演进更可维护。

一句话:“数据库里的数据结构”在代码里的对应物
注意:Entity 本身不是 NestJS 独有,它更多来自你选的 ORM/ODM。

适合干啥

  • 在数据库层做结构化建模(表字段、索引、关系)
  • 让查询/写入更可维护(至少比字符串拼 SQL 好维护一些)

何时使用

取决于你选型:

  • 用 TypeORM:你会写 @Entity() 这类实体类
  • 用 Prisma:你写的是 schema.prisma 的模型(更像“模型定义 + 类型安全访问层”,不一定叫 Entity,但在项目里的角色类似)
  • 用 Mongoose:你会写 Schema/Model(更偏 ODM)

原理(大方向)

Entity/Model 提供“结构 + 映射 + 生命周期(可选)”,最终目标是:把 DB 操作封装成更可控的接口。

伪代码示例(以 TypeORM 思路举例)

@Entity('users')
class UserEntity {
  @PrimaryGeneratedColumn()
  id: number;

  @Column()
  email: string;
}

@Injectable()
class UserRepo {
  constructor(/* inject repository */) {}
}

ORM(对象关系映射)/ ODM(对象文档映射):到底差在哪?

ORM(Object-Relational Mapping,对象关系映射)

ORM 通过把“关系型数据库的表/行/关系”映射为代码里的对象/模型,让你用更高层的接口去读写数据。
提出它的目的,是减少手写 SQL 的重复劳动,让数据访问更结构化、更易演进(当然也会带来一定抽象成本)。

  • 面向关系型数据库(MySQL/PostgreSQL…)
  • 关键词:表、行、关联、事务

ODM(Object-Document Mapping,对象文档映射)

ODM 把“文档数据库的文档/集合/嵌套结构”映射为代码里的对象/模型,帮助你更一致地读写 MongoDB 等文档型存储。
目的类似 ORM:让数据结构和访问方式更可控,只是底层存储模型不同。

  • 面向文档型数据库(MongoDB…)
  • 关键词:文档、集合、嵌套结构

何时用哪一个

别纠结名词,按存储选型走:你用 MySQL/PG 就看 ORM,你用 Mongo 就看 ODM。


CRUD(增删改查)/ REST(表述性状态转移):为什么老看到它俩一起出现?

CRUD 是什么

CRUD 是持久化系统里最常见的一组基础操作集合(Create / Read / Update / Delete)。
提出它的目的,是用一套通用词汇把“增删改查”这类需求说清楚,方便分工、设计接口与评审。

一句话:Create / Read / Update / Delete,增删改查。

REST 是什么

REST(Representational State Transfer)最初是 Roy Fielding 在博士论文里提出的一种架构风格,用“资源 + 表述 + 约束”来组织系统交互。
它的目的,是让 API 的语义更统一、可被 HTTP 的缓存/状态码等机制更好地利用。

一句话:用“资源 + HTTP 方法”来组织 API(比如 /users/:id + GET/POST/PUT/DELETE)。

在 NestJS 里怎么体现

CRUD 更像“你要做的事”,REST 更像“你怎么设计接口”。
NestJS 的 Controller 很适合写 REST 风格的 CRUD(但你也可以写 RPC 风格,不强制)。

@Controller('users')
class UserController {
  @Post() create(@Body() dto) {}
  @Get(':id') findOne(@Param('id') id) {}
  @Put(':id') update(@Param('id') id, @Body() dto) {}
  @Delete(':id') remove(@Param('id') id) {}
}

JWT(JSON Web Token)/ RBAC(基于角色的访问控制):鉴权和权限常见两套缩写

JWT(JSON Web Token,JSON Web 令牌)

JWT 是一种开放标准(RFC 7519)定义的令牌格式,用 JSON 结构承载声明(claims),常见形式是 header.payload.signature
它被广泛使用的目的,是让身份信息在服务之间可传递、可验证(尤其适合前后端分离/多服务场景)。

一句话:一种无状态 token(常用来承载登录态/身份声明)。

什么时候用:前后端分离、移动端、需要跨服务验证身份时很常见。
什么时候别硬上:你如果只是内部小系统、单体应用,Session 也完全可以。

伪代码(NestJS 常见落点):

// 认证:验证 token 的逻辑一般放 Guard / Strategy
@UseGuards(JwtAuthGuard)
@Get('me')
me(@Req() req) {
  return req.user;
}

RBAC(Role-Based Access Control,基于角色的访问控制)

RBAC 是经典的访问控制模型,用“角色(Role)”作为权限分配与授权的中间层(用户 ↔ 角色 ↔ 权限)。
目的,是让权限管理更可维护:你通常给用户分配角色,而不是给每个用户逐条配置权限。

一句话:按角色控制权限(admin/user…)。

落点通常是 Guard + 装饰器元数据:

@SetMetadata('roles', ['admin'])
@UseGuards(AuthGuard, RolesGuard)
@Delete(':id')
remove() {}

CQRS:Command Query Responsibility Segregation(命令查询职责分离,常被简化成“读写分离”)

是什么

CQRS 提倡把“改变系统状态的命令(Command)”与“读取系统状态的查询(Query)”分离成不同的模型/路径。
提出它的目的,是在复杂业务里隔离读写关注点:写侧强调规则与一致性,读侧强调展示与查询效率(但也会增加架构复杂度)。

一句话:把“写操作(Command)”和“读操作(Query)”拆成不同模型/流程

适合干啥

  • 复杂业务、读写模型差异巨大时,能让代码更清晰
  • 事件驱动/审计需求强时更常见

何时使用

别上来就 CQRS。通常是项目复杂到:

  • service 越写越像一坨“超级函数”
  • 写操作需要强规则/强审计
  • 读操作需要高度定制的视图模型 再考虑引入(NestJS 有 @nestjs/cqrs 可选)。

HTTP(超文本传输协议)/ RPC(远程过程调用):请求进来到底是哪条路?

是什么

HTTP 是应用层网络协议,用于客户端与服务端之间的请求/响应通信;RPC 是一种远程调用模型,强调“像调用本地函数一样调用远端能力”。
把它们放在一起讲的目的,是让你明确 NestJS 不只做 HTTP:它既能做 Web API,也能做微服务通信。

  • HTTP:最常见的 Web 接口方式(REST/JSON 都算在这里面)
  • RPC:更像“调用一个方法”,不强制资源风格(常见于微服务通信)

适合干啥

  • 你做 BFF / Web API:基本就是 HTTP
  • 你做服务拆分、服务间通信:RPC(或者消息队列)会更常见

何时使用(说人话)

小中型系统,先把 HTTP 写顺就行;
只有当你确实遇到“服务间调用多、边界清晰、协议要统一”时,再去考虑 RPC/Microservices 那套。

原理(在 NestJS 里怎么体现)

  • HTTP:@Controller() + @Get()/@Post() 这一套装饰器,走的是平台适配层(Express/Fastify)
  • RPC:通常用 @nestjs/microservices,通过 transport(TCP/Redis/NATS/Kafka…)收发消息

伪代码示例

// HTTP(你已经很熟了)
@Controller('users')
class UserController {
  @Get(':id')
  findOne(@Param('id') id: string) {}
}

// RPC(示意):message pattern 触发一个 handler
// @MessagePattern({ cmd: 'user.findOne' })
// findOne(payload) {}

RxJS(响应式扩展)/ Observable(可观察对象):为啥 NestJS 老出现 rxjs?

是什么

RxJS 是基于 Observable 的响应式编程库,用来表达“时间维度上的数据流”;Observable 则是可订阅的异步流抽象。
它们出现的目的,是更自然地处理“流式、可组合、可取消/重试”的异步场景(NestJS 的部分执行链路也选择了它作为抽象)。

  • RxJS:响应式编程库
  • Observable:RxJS 的核心数据类型(可以理解成“可订阅的异步流”)

适合干啥

你不用为了 NestJS 去“强行学响应式编程”。它主要在两块很常见:

  • 拦截器/管道链路next.handle().pipe(map(...)) 这种写法(你在 Interceptor 里已经见过)
  • 流式/事件式场景:SSE、WebSocket、某些需要持续推送的接口

何时使用

  • 你只写普通 CRUD:Promise/async-await 够用,别硬上 Observable
  • 你需要“组合多个异步来源”“做流式处理/取消/重试”:Observable 就很香

原理(够用版)

NestJS 允许 controller 返回:

  • primitive / object(直接返回)
  • Promise<T>(等 promise resolve)
  • Observable<T>(内部订阅,取最终值/流)

Interceptor 里之所以经常用 RxJS,是因为 CallHandler.handle() 返回的就是 Observable。

伪代码示例

// 典型 interceptor:对返回值做 map
intercept(ctx, next) {
  return next.handle().pipe(
    map(data => ({ code: 0, data })),
  );
}

// controller 返回 Observable(示意)
@Get()
list() {
  // return from([1, 2, 3]);
}

CLI:Command Line Interface(命令行接口/脚手架)

是什么

CLI(Command Line Interface)是通过命令行与工具交互的一种方式;在 NestJS 语境下通常特指官方脚手架,用于生成模板代码与维护约定结构。
它的目的,是减少重复劳动、统一项目结构,让团队协作更顺。

一句话:帮你生成模板代码、少手敲一些重复文件

适合干啥

  • 新建 module/controller/service
  • 一键生成资源(CRUD 模板),并把文件结构按约定摆好

何时使用

你如果是团队项目,建议统一用 CLI 生成骨架,代码风格更一致;
个人练手也可以不用,但熟悉一下常用命令挺省事。

原理(不深究)

CLI 本质是代码生成器(schematics),按模板生成文件 + 更新模块引用。

伪代码示例(命令示意)

nest g module user
nest g controller user
nest g service user
# nest g resource user  # 想要 CRUD 模板时再用

CORS(跨域资源共享)/ CSRF(跨站请求伪造):Web 安全里最常被混淆的两个缩写

它们是什么

CORS 是浏览器同源策略下的一套“跨域放行机制”(通过响应头协商);CSRF 是一种利用浏览器自动携带 Cookie 的攻击方式。
把它俩放一起的目的,是提醒你:跨域(CORS)和伪造请求(CSRF)是两件事,不要混着处理。

  • CORS(Cross-Origin Resource Sharing):浏览器的跨域访问控制(“能不能从别的域来调用我”)
  • CSRF(Cross-Site Request Forgery):利用浏览器自动带 Cookie 的特性发起伪造请求(“我是不是被借刀了”)

适合干啥

  • CORS:前后端分离、不同域名端口开发时必须处理
  • CSRF:当你用 Cookie 维持登录态 且接口会产生副作用(转账/下单/改资料)时,需要重点考虑

何时使用(简化判断)

  • 你用 Authorization: Bearer <token>(JWT)这类 header 携带 token:CSRF 风险通常更低(但不代表“完全不用管安全”)
  • 你用 Cookie + Session:CSRF 基本要纳入设计(同站策略、token、双重提交等)

原理(够用版)

  • CORS:是浏览器限制,你服务器得返回合适的响应头
  • CSRF:是攻击方式,你得让“跨站伪造请求”失效

伪代码示例

// CORS:NestJS 常见开启方式(示意)
app.enableCors({
  origin: ['https://your-frontend.example'],
  credentials: true,
});

最后

如果你现在只想快速上手 NestJS,我建议按这个顺序消化:

  1. 先把 IoC / DI 吃透(这决定你后面写代码是不是舒服)
  2. 再把 AOP 在 NestJS 的落点(Guard/Pipe/Interceptor/Filter)对上请求链路
  3. 然后用 DTO + ValidationPipe 把“入参质量”先稳住
  4. 至于 Entity/ORM/ODM、JWT/RBAC、CQRS,就按项目需要逐步加,不用一次性把工具箱搬回家

以上是我学习 NestJS 过程中的一些整理与理解,欢迎在评论区补充/讨论;如果哪里有偏差,也欢迎直接指出,我会及时修正。

❌