NestJS 核心概念梳理:从 Module、Controller 到 Guard、Interceptor

第一次接触 NestJS,很容易有一个感觉:

概念很多。

ModuleControllerProviderGuardPipeInterceptorException Filter......

再加上一堆装饰器:

ts 复制代码
@Module()
@Controller()
@Injectable()
@Get()
@Post()
@UseGuards()
@UseInterceptors()

如果只是跟着项目代码一个个记,很容易知道"怎么写",却说不清:

为什么 NestJS 要把一个请求拆成这么多层?

换个角度看,NestJS 最核心的内容其实可以分成两条线。

第一条是应用结构

text 复制代码
Application
   ↓
Module
   ↓
Controller
   ↓
Provider / Service
   ↓
Database / External API

它解决的是:

代码、业务能力和依赖怎么组织?

第二条是 Request Lifecycle(请求生命周期)

text 复制代码
Request
   ↓
Middleware
   ↓
Guard
   ↓
Interceptor
   ↓
Pipe
   ↓
Controller
   ↓
Response

Controller 内部通常还会继续调用 Service、Repository 或其他 Provider。

如果执行过程中出现没有被处理的异常,则会进入 Exception Filter

这篇文章就沿着这两条线,把 NestJS 中最常用、也是实际开发和面试中比较值得掌握的几个概念串起来。


1. NestJS 为什么会有这么多概念?

先不看 NestJS。

假设直接写一个创建订单接口:

ts 复制代码
app.post('/orders', async (req, res) => {
  // 检查登录状态
  // 检查用户权限
  // 校验订单参数
  // 创建订单
  // 写数据库
  // 记录日志
  // 处理异常
  // 格式化返回数据
})

项目很小的时候,这样写并没有太大问题。

但业务越来越复杂以后,一个 Route Handler(路由处理函数)很容易慢慢变成:

text 复制代码
Route Handler
├── Authentication
├── Authorization
├── Validation
├── Business Logic
├── Database
├── Logging
├── Error Handling
└── Response Transformation

代码仍然可以运行,但不同职责已经混在了一起。

以后想统一权限、统一参数校验、统一响应格式,甚至只是单独测试一段业务逻辑,都会越来越麻烦。

NestJS 的做法,是把这些职责放到更明确的位置:

text 复制代码
Module(模块)
→ 组织业务能力

Controller(控制器)
→ 接收请求

Provider(提供者) / Service(服务)
→ 承载业务能力

Guard(守卫)
→ 判断请求能不能继续

Pipe(管道)
→ 校验和转换参数

Interceptor(拦截器)
→ 包裹执行过程

Exception Filter(异常过滤器)
→ 处理未捕获异常

所以学习 NestJS 时,比记住某个装饰器怎么写更重要的是:

一段逻辑为什么应该放在这一层,而不是另一层?


2. 一个 NestJS 应用是怎么启动的?

一个基础的 NestJS 项目通常从 main.ts 开始:

ts 复制代码
import { NestFactory } from '@nestjs/core'
import { AppModule } from './app.module'

async function bootstrap() {
  const app = await NestFactory.create(AppModule)

  await app.listen(3000)
}

bootstrap()

其中最关键的是:

ts 复制代码
NestFactory.create(AppModule)

启动过程可以先简化成:

text 复制代码
main.ts
  ↓
NestFactory.create(AppModule)
  ↓
加载 AppModule
  ↓
加载其他业务 Module
  ↓
注册 Controller / Provider
  ↓
建立依赖关系
  ↓
启动应用

AppModule 是整个模块体系的入口。

后面的 Controller、Service、Provider,也都需要通过 Module 进入 NestJS 的管理范围。

所以理解 NestJS 的应用结构,可以先从 Module 开始。


3. Module:NestJS 怎么组织业务能力?

一个稍微完整一点的项目,通常不会把所有功能都放进 AppModule

例如:

text 复制代码
AppModule
├── UserModule
├── AuthModule
├── OrderModule
└── PaymentModule

再看其中的 OrderModule

text 复制代码
OrderModule
├── OrderController
├── OrderService
├── OrderRepository
└── 其他 Order Provider

Module 可以看成一个业务能力边界。

例如:

ts 复制代码
@Module({
  imports: [],
  controllers: [OrderController],
  providers: [OrderService],
  exports: [OrderService],
})
export class OrderModule {}

这里最需要理解的是四个字段。

controllers

ts 复制代码
controllers: [OrderController]

声明当前 Module 中有哪些 Controller。

例如:

text 复制代码
POST /orders
      ↓
OrderController

providers

ts 复制代码
providers: [OrderService]

声明当前 Module 管理哪些 Provider。

OrderService 注册后,NestJS 才能把它纳入当前模块的依赖关系中。

imports

如果订单模块需要使用用户模块提供的能力:

ts 复制代码
@Module({
  imports: [UserModule],
})
export class OrderModule {}

关系就是:

text 复制代码
OrderModule
    │
    │ imports
    ↓
UserModule

exports

如果 UserModule 希望把 UserService 提供给其他模块:

ts 复制代码
@Module({
  providers: [UserService],
  exports: [UserService],
})
export class UserModule {}

那么其他引入 UserModule 的模块,就可以使用它暴露出来的 UserService

text 复制代码
UserModule
   │
   │ exports UserService
   ↓
OrderModule
   │
   └── 使用 UserService

这几个字段可以这样记:

text 复制代码
providers
→ 我这里有哪些能力

exports
→ 哪些能力允许其他 Module 使用

imports
→ 我要使用哪个 Module 暴露出来的能力

controllers
→ 当前 Module 有哪些请求入口

所以 Module 不只是给文件分目录。

它还决定:

一个能力属于哪里,以及哪些模块可以使用它。


4. Controller:HTTP 请求从哪里进入?

Module 负责组织能力,真正接住 HTTP 请求的通常是 Controller。

例如:

ts 复制代码
@Controller('orders')
export class OrderController {
  constructor(
    private readonly orderService: OrderService,
  ) {}

  @Post()
  create(@Body() dto: CreateOrderDto) {
    return this.orderService.create(dto)
  }
}

这里:

ts 复制代码
@Controller('orders')

声明 Controller 的路由前缀。

而:

ts 复制代码
@Post()

表示当前方法处理 POST 请求。

于是:

text 复制代码
POST /orders
     ↓
OrderController.create()

常见的 Controller 装饰器还有:

ts 复制代码
@Get()
@Post()
@Put()
@Delete()

@Param()
@Query()
@Body()

这些 API 本身并不难。

更值得关注的是 Controller 的职责边界。

通常推荐:

text 复制代码
Controller
   ↓
接收 HTTP 参数
   ↓
调用 Service
   ↓
返回结果

而不是:

text 复制代码
Controller
├── 查询数据库
├── 写大量业务逻辑
├── 调多个第三方接口
├── 自己处理权限
└── 自己兜底所有异常

例如:

ts 复制代码
@Post()
create(@Body() dto: CreateOrderDto) {
  return this.orderService.create(dto)
}

Controller 负责 HTTP 层,真正的订单业务交给 OrderService


5. Provider 与 DI:Service 为什么可以直接注入?

前面的 Controller 中有这样一段代码:

ts 复制代码
constructor(
  private readonly orderService: OrderService,
) {}

第一次看到时可能会有点奇怪:

OrderService 明明没有手动 new,它是从哪里来的?

这里就涉及 NestJS 很重要的一套机制:

Dependency Injection(依赖注入,简称 DI)。

5.1 先弄清楚什么叫"依赖"

假设 OrderController 创建订单时必须调用 OrderService

text 复制代码
OrderController
      ↓
需要
      ↓
OrderService

那么就可以说:

OrderController 依赖 OrderService

如果不用框架,最直接的写法就是自己创建:

ts 复制代码
export class OrderController {
  private readonly orderService = new OrderService()
}

这时候 Controller 同时负责了两件事:

text 复制代码
我要使用 OrderService
+
我自己负责创建 OrderService

代码当然可以运行。

但随着项目变复杂,OrderService 自己可能还依赖:

text 复制代码
OrderRepository
CacheService
PaymentClient

如果全部自己创建,代码可能慢慢变成:

ts 复制代码
const repository = new OrderRepository()
const cache = new CacheService()
const payment = new PaymentClient()

const orderService = new OrderService(
  repository,
  cache,
  payment,
)

const controller = new OrderController(
  orderService,
)

依赖越多,创建和维护这些对象就越麻烦。

5.2 DI 做了什么?

依赖注入换了一种思路:

类只声明"我需要什么",至于这个对象怎么创建,交给外部处理。

所以 Controller 只需要写:

ts 复制代码
constructor(
  private readonly orderService: OrderService,
) {}

相当于告诉 NestJS:

text 复制代码
OrderController
      │
      │ 我需要 OrderService
      ↓
NestJS

NestJS 的 DI Container(依赖注入容器) 会根据已经注册的 Provider 找到 OrderService

text 复制代码
OrderController
      │
      │ 需要 OrderService
      ↓
Nest DI Container
      │
      ├── 找到 OrderService Provider
      │
      ├── 创建 / 获取实例
      │
      ↓
注入 OrderController

所以"依赖注入"这个词虽然听起来比较抽象,实际做的事情可以概括成:

一个类需要另一个对象时,不自己创建,而是由外部把需要的对象提供进来。

在 NestJS 里,这个"外部"主要就是 DI Container。

5.3 Provider 又是什么?

最常见的 Provider 就是 Service:

ts 复制代码
@Injectable()
export class OrderService {
  create(dto: CreateOrderDto) {
    // 创建订单
  }
}

@Injectable() 让这个类可以参与 NestJS 的依赖注入体系。

但 Provider 并不只等于 Service。

下面这些能力都可能注册成 Provider:

text 复制代码
Service
Repository
Factory
Adapter
External API Client
Cache Client

所以可以把 Provider 理解成:

交给 NestJS 管理,并且可以被其他对象注入使用的能力。

5.4 为什么要这样做?

最直观的好处是:

  • 类不再负责创建自己的依赖
  • 单元测试时更容易替换 Mock
  • 更容易替换具体实现
  • Provider 的实例和生命周期可以统一管理

例如测试时,可以把:

text 复制代码
OrderService

换成:

text 复制代码
MockOrderService

而 Controller 本身不需要跟着修改。

5.5 再往下一层:Custom Provider

Provider 也不一定只能这样注册:

ts 复制代码
providers: [OrderService]

还可以:

ts 复制代码
{
  provide: 'PAYMENT_SERVICE',
  useClass: StripePaymentService,
}

这里建立的是一层映射:

text 复制代码
Injection Token(注入令牌)
        ↓
具体 Provider

NestJS 还支持:

text 复制代码
useClass
useValue
useFactory
useExisting

这部分属于 DI 更深入的内容,这篇先不展开。

到这里,可以把 Module 和 DI 的职责分开:

text 复制代码
Module
→ 决定 Provider 在哪里注册、能被谁使用

DI Container
→ 负责找到、创建并注入 Provider

6. 一次 HTTP 请求到底怎么经过 NestJS?

前面解决的是:

NestJS 怎么组织代码,以及对象之间怎么建立依赖。

接下来换到第二条线:

一个请求真正进入应用以后,会经过哪些处理?

仍然使用同一个接口:

http 复制代码
POST /orders

先看整体链路:

text 复制代码
HTTP Request
      ↓
Middleware
      ↓
Guard
      ↓
Interceptor Before
      ↓
Pipe
      ↓
Controller
      ↓
Interceptor After
      ↓
HTTP Response

Controller 内部通常还会调用 Service、Repository 等 Provider,这属于应用自己的业务调用过程。

如果请求执行过程中出现未处理异常:

text 复制代码
Exception
   ↓
Exception Filter
   ↓
Error Response

接下来的几个组件,就是在这条链路的不同位置解决不同问题。


7. Middleware:请求刚进来先做什么?

Middleware(中间件)位于请求链比较靠前的位置。

假设希望每个请求先生成一个 Request ID,或者记录最基础的访问日志,就很适合放在这里。

例如:

ts 复制代码
@Injectable()
export class LoggerMiddleware
  implements NestMiddleware {
  use(req: Request, res: Response, next: NextFunction) {
    console.log(req.method, req.url)

    next()
  }
}

常见用途包括:

text 复制代码
Request ID
基础访问日志
Header 处理
请求上下文初始化

例如:

text 复制代码
POST /orders
      ↓
RequestIdMiddleware
      ↓
生成 requestId
      ↓
继续执行

Middleware 更接近底层 HTTP 请求。

但如果接下来需要判断:

当前用户到底有没有权限访问这个具体接口?

这件事交给 Guard 会更合适。


8. Guard:这个请求能不能继续?

Guard(守卫)的职责比较明确:

判断当前请求是否允许继续进入后续 Handler(处理函数)。

假设 POST /orders 只有登录用户才能访问:

ts 复制代码
@Injectable()
export class AuthGuard implements CanActivate {
  canActivate(context: ExecutionContext) {
    const request = context
      .switchToHttp()
      .getRequest()

    return Boolean(request.user)
  }
}

可以简化成:

text 复制代码
POST /orders
      ↓
AuthGuard
      │
      ├── true → 继续
      │
      └── false → 拒绝

Guard 常用于:

text 复制代码
Authentication(认证)
Authorization(授权)
JWT
Role
Permission
API Key

例如:

text 复制代码
POST /orders
      ↓
AuthGuard
      ↓
检查登录状态
      ↓
RoleGuard
      ↓
检查 create:order 权限
      ↓
继续执行

Guard 还可以作用在不同范围:

text 复制代码
Global
  ↓
Controller
  ↓
Route

例如整个 Controller:

ts 复制代码
@UseGuards(AuthGuard)
@Controller('orders')
export class OrderController {}

也可以只限制某一个接口。

如果一段逻辑主要回答:

"这个请求有没有资格继续?"

Guard 通常就是合适的位置。


9. Pipe:通过权限以后,参数能不能用?

权限检查通过后,下一步就是客户端带进来的参数。

Pipe(管道)主要负责两件事情:

text 复制代码
Validation(校验)
+
Transformation(转换)

参数转换

例如:

ts 复制代码
@Get(':id')
findOne(
  @Param('id', ParseIntPipe) id: number,
) {}

请求:

text 复制代码
GET /orders/123

URL 里的参数原本是字符串:

ts 复制代码
'123'

经过 ParseIntPipe 后,可以得到:

ts 复制代码
123

DTO + ValidationPipe

实际项目里更常见的是 DTO(Data Transfer Object,数据传输对象)校验。

例如:

ts 复制代码
export class CreateOrderDto {
  @IsString()
  productId: string

  @IsInt()
  @Min(1)
  quantity: number
}

然后配置:

ts 复制代码
app.useGlobalPipes(
  new ValidationPipe({
    transform: true,
    whitelist: true,
  }),
)

这样请求进入 Controller 之前,就可以先完成:

text 复制代码
字段校验
类型转换
无效字段处理

Guard 和 Pipe 的区别也就比较直观:

text 复制代码
Guard
→ 这个请求有没有资格继续

Pipe
→ 请求带进来的参数能不能使用

10. Interceptor:为什么它能包住 Controller?

权限和参数都没问题后,请求马上就要进入 Controller。

假设我们希望统计:

text 复制代码
POST /orders 执行耗时

或者统一处理响应结构、日志、缓存、超时。

这类逻辑通常不属于具体的订单业务,而是希望围绕整个 Handler 的执行过程处理。

这时就很适合使用 Interceptor(拦截器)。

Interceptor 一个很重要的特点是:

它可以包裹 Handler 的整个执行过程。

例如:

text 复制代码
Interceptor Before
        ↓
Controller
        ↓
Service
        ↓
Interceptor After

看一个请求耗时统计的例子:

ts 复制代码
@Injectable()
export class LoggingInterceptor
  implements NestInterceptor {

  intercept(
    context: ExecutionContext,
    next: CallHandler,
  ) {
    const start = Date.now()

    return next.handle().pipe(
      tap(() => {
        console.log(
          `cost: ${Date.now() - start}ms`,
        )
      }),
    )
  }
}

请求进入时,Interceptor 先记录开始时间。

然后:

ts 复制代码
next.handle()

把执行继续交给后面的 Handler。

Controller 和 Service 执行完成后,响应还会再次经过 Interceptor:

text 复制代码
LoggingInterceptor Before
        ↓
Controller
        ↓
Service
        ↓
返回结果
        ↓
LoggingInterceptor After
        ↓
计算请求耗时

所以 Interceptor 很适合处理:

text 复制代码
请求耗时
Logging
Response Transformation
Cache
Timeout
数据脱敏

为什么 NestJS Interceptor 使用 RxJS?

第一次看到 NestJS Interceptor 时,很多开发者会有一个疑问:

平时写 Node.js 业务代码,我们经常使用:

ts 复制代码
async/await
Promise

为什么这里:

ts 复制代码
next.handle()

返回的是:

ts 复制代码
Observable

而不是 Promise?

这里涉及 RxJS。

RxJS 是一个用于处理异步数据流的 JavaScript 库。

NestJS 在 Interceptor 中使用 Observable(可观察对象)作为统一的执行结果抽象,这样 Interceptor 不只是等待一个最终值,还可以继续对整个返回过程做组合处理,例如:

  • 响应转换
  • 日志
  • 异常处理
  • 超时
  • 缓存

可以先把 Promise 和 Observable 粗略区分成:

text 复制代码
Promise
→ 一次异步操作
→ 最终得到一个结果

而 Observable 更像:

text 复制代码
异步执行过程
      ↓
数据进入处理链
      ↓
可以继续转换 / 监听 / 处理异常
      ↓
结束

这里不需要把 Observable 理解成"NestJS 接口一定会返回很多次数据"。

对于 Interceptor 来说,更重要的是:

Observable 提供了一套可以继续组合和处理异步执行结果的机制。


next.handle().pipe() 到底是什么?

回到这段代码:

ts 复制代码
return next.handle().pipe(
  tap(() => {
    console.log('request finished')
  }),
)

这里有三个关键点。

next.handle()

表示继续执行后面的 Handler,并得到后续执行结果对应的 Observable:

text 复制代码
Interceptor
      ↓
next.handle()
      ↓
Controller
      ↓
Service
      ↓
返回结果

pipe()

这里的:

ts 复制代码
pipe()

不是前面讲的 NestJS Pipe(管道)

它是 RxJS 用来组合数据流处理逻辑的方法。

这两个名字很像,但属于完全不同的概念:

text 复制代码
NestJS Pipe
→ 请求参数校验 / 转换

RxJS pipe()
→ 组合 Observable 的处理逻辑

tap()

tap() 可以用于观察执行过程,而不改变原本的数据。

例如:

ts 复制代码
tap(() => {
  console.log('request finished')
})

很适合做:

text 复制代码
日志
性能统计
埋点

如果需要修改真正返回给客户端的数据,则经常会看到其他 RxJS Operator(操作符),例如 map()


实际 NestJS 项目里会大量使用 RxJS 吗?

不一定。

普通 CRUD 业务代码里,大量代码仍然是:

ts 复制代码
@Get()
async findAll() {
  return this.userService.findAll()
}

也就是熟悉的:

text 复制代码
async / await + Promise

RxJS 更容易在下面这些地方出现:

  • Interceptor
  • WebSocket
  • 微服务通信
  • 消息流
  • 其他需要组合异步数据流的场景

所以第一次学习 NestJS 时,不需要为了看懂 Interceptor 先完整学习 RxJS。

先弄清这一点就够了:

next.handle() 返回 Observable,Interceptor 可以借助 RxJS 对后续 Handler 的执行结果继续加工。


多个 Interceptor 会怎么执行?

假设存在:

text 复制代码
Interceptor A
Interceptor B
Controller

执行过程类似:

text 复制代码
A before
   ↓
B before
   ↓
Controller
   ↓
B after
   ↓
A after

这种结构经常被称为"洋葱模型"。

也正因为这种执行方式,Interceptor 很适合处理围绕 Handler 的横切逻辑。


11. Exception Filter:如果中间出错了怎么办?

前面的流程都正常时,请求会继续得到响应。

但假设 OrderService 查询不到商品:

ts 复制代码
throw new NotFoundException(
  'Product not found',
)

如果这个异常没有在业务代码中处理,就会进入异常处理流程。

Exception Filter(异常过滤器)可以负责把异常转换成最终的 HTTP 错误响应:

text 复制代码
OrderService
     ↓
throw Exception
     ↓
Exception Filter
     ↓
Error Response

常见用途包括:

text 复制代码
统一错误结构
记录异常日志
业务异常映射
设置 HTTP Status Code
隐藏敏感错误信息

例如最终返回:

json 复制代码
{
  "code": 404,
  "message": "Product not found"
}

这里需要区分一点:

成功请求不会固定经过 Exception Filter,它主要在未处理异常出现时介入。


12. Decorator、Metadata 和 ExecutionContext 是怎么串起来的?

前面的 Guard 中已经出现过:

ts 复制代码
ExecutionContext

NestJS 项目里还经常能看到这样的代码:

ts 复制代码
@Roles('admin')
@Post('/orders')
create() {}

为什么 Guard 能知道:

当前接口要求 admin 权限?

这背后涉及几个相关概念。

Decorator(装饰器)

例如:

ts 复制代码
@Roles('admin')

可以给当前 Handler 声明额外信息。

Metadata(元数据)

这些信息可以作为 Metadata 保存,例如:

text 复制代码
roles = ['admin']

ExecutionContext(执行上下文)

运行时,Guard 或 Interceptor 可以通过 ExecutionContext 知道:

text 复制代码
当前 Controller
当前 Handler
当前 Request
当前运行环境

例如 HTTP 场景:

ts 复制代码
const request = context
  .switchToHttp()
  .getRequest()

Reflector(反射工具)

再通过 Reflector 读取 Handler 或 Controller 上保存的 Metadata。

整个关系可以整理成:

text 复制代码
Decorator
   ↓
Metadata
   ↓
Reflector
   ↓
Guard / Interceptor

ExecutionContext
   ↓
提供当前正在执行的上下文

例如权限 Guard 最终判断的就是:

text 复制代码
当前用户拥有的角色
        +
当前接口要求的角色
        ↓
是否允许继续

这也是 Guard 比普通 Middleware 更适合做 Handler 级权限控制的重要原因。


13. Middleware、Guard、Pipe、Interceptor 到底怎么选?

学完这些组件以后,很容易产生一个问题:

日志是不是 Middleware 和 Interceptor 都能做?

可以。

真正需要判断的不是"能不能写",而是:

这段逻辑放在请求生命周期的哪个位置最合适?

可以用下面这张表快速区分:

组件 主要解决的问题 常见场景
Middleware 请求刚进来先做什么? Request ID、基础日志
Guard 能不能继续? 登录、权限
Pipe 参数能不能用? 校验、类型转换
Interceptor 执行前后做什么? 耗时、响应转换、缓存
Exception Filter 出错以后怎么办? 统一异常响应

如果只想快速复习,可以记成:

text 复制代码
Middleware
→ 请求先预处理

Guard
→ 能不能进

Pipe
→ 参数能不能用

Interceptor
→ 执行前后包一层

Exception Filter
→ 出错以后怎么收口

这些组件并不是互相替代,而是在请求生命周期的不同位置承担不同职责。


14. 用 POST /orders 把整条链串起来

前面已经把各个组件分别讲过了,这里不再重新解释,只把它们放回同一个请求中。

客户端发送:

http 复制代码
POST /orders

请求体:

json 复制代码
{
  "productId": "A001",
  "quantity": 2
}

完整流程可以整理成:

text 复制代码
POST /orders
      ↓
RequestIdMiddleware
生成 requestId

      ↓
AuthGuard / RoleGuard
检查认证和权限

      ↓
LoggingInterceptor Before
记录开始时间

      ↓
ValidationPipe
校验 CreateOrderDto

      ↓
OrderController
接收参数并调用 OrderService

      ↓
OrderService
创建订单

      ↓
LoggingInterceptor After
记录请求耗时

      ↓
Response
返回订单数据

如果其中出现没有处理的异常:

text 复制代码
Unhandled Exception
        ↓
Exception Filter
        ↓
Error Response

到这里,前面看起来比较零散的 NestJS 概念,就已经放回了同一条真实请求链里。


15. NestJS 面试重点快速复习

这一部分不再重新展开正文,只保留适合面试前快速复习的版本。

1. Module 主要解决什么问题?

Module 用来组织 Controller、Provider 以及模块之间的能力边界,同时影响 Provider 在不同 Module 之间的可见性。

2. importsproviderscontrollersexports 怎么区分?

text 复制代码
imports
→ 引入其他 Module

controllers
→ 注册当前 Module 的 Controller

providers
→ 注册当前 Module 的 Provider

exports
→ 把 Provider 暴露给其他 Module

3. Provider 是什么?

Provider 是由 NestJS 容器管理,并且可以通过依赖注入被其他对象使用的能力。

Service 是最常见的一种 Provider。

4. 什么是 DI?

DI 把"使用依赖"和"创建依赖"分开。

类只声明自己需要什么,由 NestJS DI Container 负责查找、创建并注入对应 Provider。

5. Controller 和 Service 怎么分工?

Controller 主要负责 HTTP 层:

text 复制代码
接收参数
↓
调用 Service
↓
返回结果

Service 承担主要业务逻辑。

6. 一次 HTTP 请求会经过哪些主要阶段?

可以先回答:

text 复制代码
Middleware
↓
Guard
↓
Interceptor Before
↓
Pipe
↓
Controller
↓
Interceptor After
↓
Response

Controller 内部通常会继续调用 Service 等 Provider。

如果出现未处理异常,再进入 Exception Filter。

7. Middleware 和 Guard 有什么区别?

Middleware 更偏请求进入时的通用 HTTP 预处理。

Guard 更关注当前请求是否允许执行具体 Handler,并且可以结合 ExecutionContext 和 Metadata 做细粒度权限判断。

8. Guard 和 Pipe 有什么区别?

text 复制代码
Guard
→ 请求有没有资格继续

Pipe
→ 参数是否合法、是否需要转换

9. Guard 和 Interceptor 有什么区别?

Guard 主要决定:

要不要继续执行。

Interceptor 主要处理:

Handler 执行前后要做什么。

10. 为什么 Interceptor 能处理执行前和执行后?

因为它通过:

ts 复制代码
next.handle()

包裹后续 Handler 的执行,并可以借助 RxJS 继续处理返回的 Observable。

11. Exception Filter 什么时候执行?

请求执行过程中出现未处理异常时,Exception Filter 可以接管并生成最终错误响应。

成功请求不会固定经过它。

12. ExecutionContextReflector 有什么作用?

ExecutionContext 提供当前 Controller、Handler、Request 等运行时上下文。

Reflector 用来读取 Decorator 写入的 Metadata,经常用于权限控制等场景。


16. 总结:先把 NestJS 主干串起来

回头看整篇文章,NestJS 最重要的内容还是两条线。

第一条是应用结构:

text 复制代码
Application
   ↓
Module
   ↓
Controller
   ↓
Provider / Service
   ↓
DI

第二条是请求生命周期:

text 复制代码
Request
   ↓
Middleware
   ↓
Guard
   ↓
Interceptor
   ↓
Pipe
   ↓
Controller
   ↓
Interceptor
   ↓
Response

Unhandled Exception
        ↓
Exception Filter

理解 NestJS 的重点,并不是记住多少个 @xxx 装饰器。

更重要的是,当项目里出现一段逻辑时,能够判断:

它属于应用结构里的哪一层,又应该放在请求生命周期的哪个位置?

这套主干理解清楚以后,再继续学习下面这些能力会自然很多:

  • Custom Provider
  • Provider Scope
  • Dynamic Module
  • Circular Dependency / forwardRef
  • Lifecycle Hooks
  • ModuleRef
  • Testing
  • Configuration
  • Authentication / Authorization

例如:

ts 复制代码
ConfigModule.forRoot()
JwtModule.register()

背后会继续涉及 Dynamic Module(动态模块)。

而:

text 复制代码
DEFAULT
REQUEST
TRANSIENT

则属于 Provider Scope(Provider 作用域)。

这些内容不需要一次全部掌握。

先把 Module、DI 和请求生命周期真正串起来,再往下深入,NestJS 的整体设计就不会再显得那么零散。

相关推荐
东风破_2 小时前
JWT 1:从一个登录请求开始,理解 React 项目里的 API 层与 Mock
前端·后端
东风破_2 小时前
JWT 3:为什么 Token 要放进 Authorization?Axios 拦截器到底解决了什么?
前端·后端
东风破_2 小时前
JWT 5:路由守卫是什么?把整个 JWT 登录鉴权流程串起来
前端·后端
东风破_2 小时前
JWT 2:HTTP 是无状态的,为什么登录成功后还要给 Token?
前端·后端
东风破_2 小时前
JWT 4:Zustand 到底解决了什么?为什么登录状态要放进 Store?
前端·后端
IT_陈寒2 小时前
Vite动态导入差点让我秃头,原来问题出在这
前端·人工智能·后端
唐青枫3 小时前
别只把大括号当作用域:Zig Block、标签块与控制流实战
后端
TunerT_TQ5 小时前
Microsoft |Playwright CLI 源码静态审阅:从 5 个文件看浏览器自动化工具的工程边界
后端·开源·github