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

10 阅读10分钟

你在 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(方便定制参数)”。