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

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

概念很多。

Module、Controller、Provider、Guard、Pipe、Interceptor、Exception 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. imports、providers、controllers、exports 怎么区分?

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. ExecutionContext 和 Reflector 有什么作用?

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 的整体设计就不会再显得那么零散。

相关推荐
金銀銅鐵20 小时前
[Java] 用 GUI 展示 EnumSet 增减元素时的内部变化(上)
java·后端·ai编程
念何架构之路20 小时前
go-grpc设计思想
开发语言·后端·golang
system_error_x21 小时前
SpringBoot 集成 Thymeleaf 简洁入门
java·spring boot·后端
步行cgn1 天前
Spring MVC 视图机制的核心接口与类
java·开发语言·后端·spring·mvc
码事漫谈1 天前
AI 读你的提示词,其实很没耐心
后端
用户8356290780511 天前
如何使用 Python 格式化 Excel 单元格中的长文本
后端·python
我命由我123451 天前
Python 开发 - Python 的 pathlib
java·开发语言·后端·python·学习·java-ee·学习方法
IT_陈寒1 天前
Java性能优化完全指南
前端·人工智能·后端
电子IT笔记1 天前
对接群晖NAS,上传和删除踩坑记录
前端·后端
Mk598781 天前
P4:把 Agent 的记忆搬进 Redis —— AgentScope-Java 工程化实战
后端