CodeRoadMap
首页路线题库资源库文章社区
CodeRoadMap

程序员的学习成长路线图。内容免费开放,登录后可同步路线图与课时进度。

由 程序员柏风 独立维护 · 关于作者

学习

  • 路线图
  • 课程
  • 面试题
  • 文章
  • 我

更多

  • 资源
  • 社区
  • 关于作者
  • 内容说明

© 2026 CodeRoadMap

津ICP备2026012044号-1·津公网安备12011002026155号·coderoadmap@126.com
NestJS 实战入门/课时/Module 与依赖注入
RawBlame
课时目录12 节

NestJS 实战入门

  1. 01NestJS 是什么
  2. 02项目创建与结构
  3. 03Module 与依赖注入
  4. 04Controller 与路由
  5. 05Service 层
  6. 06Pipe 与校验
  7. 07Guard 与认证
  8. 08ConfigModule
  9. 09数据库接入入门
  10. 10异常与过滤器
  11. 11测试
  12. 12综合练习

Module 与依赖注入

@Module、providers、exports、与 Spring IoC 对照。

35 分钟

Module 与依赖注入

这一课你将能做到

  1. 写出带 controllers / providers / exports 的特征模块(题库模块、作答模块)
  2. 用构造器注入 Service,并解释默认单例作用域
  3. 故意漏注册 Provider,读懂启动报错并独立修好

为什么要学

Module(模块)是 Nest 的组织单元。人话:一个业务包(如题库 questions)把自己的 Controller、Service 列在 @Module({...}) 清单里;需要把 Service 分享给别的包时,再通过 exports 显式开放。

依赖注入(DI,Dependency Injection)人话:类不自己 new 依赖,而是在构造器里声明「我需要 QuestionsService」,由 Nest 容器创建实例并塞进来。好处是依赖关系一目了然、单元测试时可以随时换成 mock。

这和 Spring 的 IoC 容器、@Service Bean 是同一套思想,只是语法换成了 TypeScript 装饰器。

先建立图像

AppModule
  imports: [QuestionsModule, SubmissionsModule]
                    │
                    ▼
            QuestionsModule
              controllers: [QuestionsController]
              providers:   [QuestionsService]
              exports:     [QuestionsService]  ← 别人 import 后才能注入

SubmissionsModule
  imports: [QuestionsModule]   ← 判分要查题目标准答案
              │
              ▼
SubmissionsService
  constructor(private readonly questions: QuestionsService) {}

@Module 四个字段:

字段作用Spring 类比
imports引入其他模块导出的能力引入其他 Configuration
controllers本模块的 HTTP 入口集合@RestController Bean
providers本模块内部可注入的类@Service / @Component
exports把本模块 Provider 开放给其他模块公开暴露 Bean

跟着做

步骤 1:用 CLI 生成题库资源

在 quiz-api 项目根目录执行:

nest g module questions
nest g controller questions --no-spec
nest g service questions --no-spec

src/questions/ 下会出现三个文件,且新版本 CLI 会自动把 QuestionsModule 加入根模块的 imports。再生成作答模块:

nest g module submissions
nest g service submissions --no-spec
nest g controller submissions --no-spec

步骤 2:写最小 Service 与 Module

src/questions/questions.service.ts:

import { Injectable } from "@nestjs/common";

export interface Question {
  id: number;
  stem: string; // 题干
  options: string[]; // 选项
  answerIndex: number; // 正确选项下标
  difficulty: "easy" | "medium" | "hard";
}

@Injectable()
export class QuestionsService {
  private items: Question[] = [
    {
      id: 1,
      stem: "TypeScript 中,用来描述对象形状的类型是?",
      options: ["interface", "class", "enum", "namespace"],
      answerIndex: 0,
      difficulty: "easy",
    },
  ];

  findAll(): Question[] {
    return this.items;
  }

  findOne(id: number): Question | undefined {
    return this.items.find((q) => q.id === id);
  }

  getAnswerIndex(id: number): number {
    const q = this.findOne(id);
    return q ? q.answerIndex : -1;
  }
}

src/questions/questions.module.ts:

import { Module } from "@nestjs/common";
import { QuestionsController } from "./questions.controller";
import { QuestionsService } from "./questions.service";

@Module({
  controllers: [QuestionsController],
  providers: [QuestionsService],
  exports: [QuestionsService], // 作答模块要靠它判分
})
export class QuestionsModule {}

步骤 3:Controller 构造器注入

src/questions/questions.controller.ts:

import { Controller, Get, Param } from "@nestjs/common";
import { QuestionsService } from "./questions.service";

@Controller("questions")
export class QuestionsController {
  // 人话:不要 new QuestionsService(),交给容器
  constructor(private readonly questions: QuestionsService) {}

  @Get()
  findAll() {
    return this.questions.findAll();
  }

  @Get(":id")
  findOne(@Param("id") id: string) {
    return this.questions.findOne(Number(id));
  }
}

启动后访问 http://localhost:3000/questions,应看到种子题目数组。

步骤 4:制造并修复「未注册 Provider」

临时把 QuestionsModule 中 providers 里的 QuestionsService 删掉,保存。终端会报类似错误:

Nest can't resolve dependencies of the QuestionsController (?).
Potential solutions:
- Is QuestionsService a provider?
- If ... add it to the providers list.

加回 providers: [QuestionsService] 即恢复。结论:这类报错指向装配清单,不是路由写错。

步骤 5:跨模块注入(作答模块判分)

src/submissions/submissions.module.ts:

import { Module } from "@nestjs/common";
import { QuestionsModule } from "../questions/questions.module";
import { SubmissionsController } from "./submissions.controller";
import { SubmissionsService } from "./submissions.service";

@Module({
  imports: [QuestionsModule], // 必须导入,才能看到 exports
  controllers: [SubmissionsController],
  providers: [SubmissionsService],
})
export class SubmissionsModule {}

src/submissions/submissions.service.ts:

import { Injectable } from "@nestjs/common";
import { QuestionsService } from "../questions/questions.service";

export interface Submission {
  questionId: number;
  userId: number;
  selectedIndex: number;
  correct: boolean;
}

@Injectable()
export class SubmissionsService {
  private records: Submission[] = [];

  constructor(private readonly questions: QuestionsService) {}

  submit(questionId: number, userId: number, selectedIndex: number): Submission {
    const answerIndex = this.questions.getAnswerIndex(questionId);
    const record: Submission = {
      questionId,
      userId,
      selectedIndex,
      correct: answerIndex === selectedIndex,
    };
    this.records.push(record);
    return record;
  }
}

Nest 的可见性是模块边界:只在根模块引入没用,使用方模块自己也必须 imports。

步骤 6:确认默认单例作用域

在任意两处注入 QuestionsService,默认拿到的是同一个实例(整个应用生命周期内单例)。所以 Service 上的内存数组会被所有请求共享——演示方便,但多实例部署时不要把它当数据库。

步骤 7:认识自定义 Provider(选学形状)

除了直接写类,Provider 还可以是值、工厂:

@Module({
  providers: [
    QuestionsService,
    { provide: "PASS_SCORE", useValue: 60 },
    {
      provide: "ID_GENERATOR",
      useFactory: () => () => Math.floor(Math.random() * 1_000_000),
    },
  ],
  exports: ["PASS_SCORE"],
})
export class QuestionsModule {}

注入字符串令牌时配合 @Inject("PASS_SCORE")。测试中最常用的 useValue 就是这个机制。

讲清楚要点

  1. 一个业务域一个 Module:题库、作答各自成包,避免全部塞进 AppModule。
  2. 按构造器类型解析依赖:参数类型必须对应一个已注册 Provider,配合 emitDecoratorMetadata 才能工作。
  3. 跨模块两步走:提供方 exports,使用方 imports,缺一不可。
  4. 默认单例:可变内存状态会被所有请求共享,入门阶段避免在生产依赖这一特性。
  5. 测试红利来自 DI:换 Provider 实现(mock)即可控制依赖行为,和 Spring 替换 Bean 的思路一致。
  6. 显式优于自动扫描:Nest 不像 Spring 靠包扫描自动发现一切,模块树更啰嗦,但边界更清晰。

你可能会懵的地方

  • providers 和 exports 有何区别? providers 让本模块内部能注入;exports 允许其他模块注入。只 providers 不 exports,外部模块拿不到。

  • 为什么不能把别人的 Service 直接写进自己的 providers? 重复登记会产生两个实例、破坏单一所有权,也违背模块边界。正确做法是 import 对方模块、使用其 exports。

  • 两个模块互相引用会怎样? 形成循环依赖,容器初始化时可能报错。初学者遇到这种情况优先重构边界;实在拆不开时才用 forwardRef 应急。

  • @Injectable() 的括号能省略吗? 带参数时必须括号;规范写法统一加括号。被注入的类都应该标注它。

  • Nest 为什么不像 Spring 那样自动扫描所有 @Service? Nest 选择显式模块树,强制你声明依赖与可见性,大型项目里边界更可控、也更容易做按需加载。

错法对照

错法问题正解
Controller 里 new QuestionsService()绕过容器、单例失效、难测试构造器注入
跨模块用 Service 却不 exports运行期解析失败exports + 对方 imports
所有类都塞进 AppModule根模块膨胀、边界消失按业务域拆特征模块
漏写 @Injectable()元数据缺失、注入异常补装饰器并注册
用 providers 重复登记别的模块的 Service出现多实例、状态不一致通过模块导出共享

卡点急救

  1. 报 can't resolve dependencies:按报错中的类名,依次检查它的构造器参数类型是否已在当前模块 providers 或 imports 的 exports 中。
  2. 注入得到 undefined:检查循环依赖、参数属性写法(private readonly x: X)以及是否误用了 interface 当运行时令牌。
  3. CLI 没有自动更新 AppModule:确认生成命令在项目根目录执行;手工在 AppModule 的 imports 补上即可。

面试常问

  • NestJS 的模块可见性规则是什么? Service 默认仅在声明它的模块内可见;必须在 exports 中暴露、消费模块在 imports 中引入后才能跨模块注入。这与 Spring Bean 默认全局可见不同。

  • NestJS Provider 的默认作用域是什么? 默认是 DEFAULT(应用级单例)。此外还有 REQUEST(每请求一个实例)和 TRANSIENT(每次注入新建),后两者有性能开销,按需使用。

  • 依赖注入在测试时带来什么好处? 可以用 useValue / useClass 把数据库、网络客户端替换成假实现,让单元测试快速、确定、不依赖外部环境。

自测三问

  1. providers 与 exports 分别解决谁的「看得见」问题?
  2. 启动报无法解析 QuestionsController 依赖时,你先查哪两处?
  3. 默认单例作用域对放在 Service 字段里的内存数据有什么影响?

小结一句话

Module 打包 Controller 与 Provider,DI 按构造器类型注入,跨模块协作靠 exports 加 imports。

延伸

  • Modules(官方模块文档)
  • Providers / Dependency Injection
  • 下一课预告:学习 Controller 与路由细节,用 @Param / @Query / @Body 取参,用 DTO 描述创建题目的请求体。
加载本节练习…
← 上一课项目创建与结构下一课 →Controller 与路由