← 全部材料 Nest
NESTJS · 16 个高频点

它把 Express 的自由换成了约定

Nest 的所有设计都在做同一件事:用一套可分层的约定,把"什么都自己做主"的 Express 变成"结构固定、边界清晰"的工程框架。模块是边界,DI 是装配, 五个可插拔组件(中间件、守卫、拦截器、管道、过滤器)是横切关注点的落点。

01先猜再看看到代码先在心里跑一遍,再看下面的结果。
02抓一个主线如果只记一件事,记请求生命周期那条链,其他都是它的补充。
03连上 NodeDI、装饰器、元数据这些机制,底层原理在 Node 那份。
01

三个角色,一条直线

module wires, controller routes, service works
Controller 只负责"接请求、调方法、返回结果",一行业务逻辑都不该有。 业务在 Service,装配在 Module。这个边界守住了,Nest 的价值才成立。

为什么 Controller 要"薄"

  • 路由和业务解耦,同一份业务能被 HTTP、微服务、定时任务复用
  • 单元测试时不用起 HTTP 服务,直接测 Service
  • 横切逻辑(日志、鉴权、缓存)交给守卫和拦截器,不用污染每个方法

常见的写法错误

  • 在 Controller 里直接 this.repo.save()
  • 在 Controller 里手写 try/catch 拼错误响应
  • Service 里直接读 req(它不该知道 HTTP)
面试这样答
Nest 把应用拆成 Module、Controller、Provider 三层。Module 负责组织,声明这个模块有哪些控制器、提供者和导出;Controller 只负责路由映射和参数解析,把请求交给 Service;Provider(通常是 Service)承载业务逻辑。这样分层的好处是业务逻辑不依赖 HTTP,可以被不同入口复用,也更容易做单元测试。横切关注点则通过中间件、守卫、拦截器、管道、过滤器这五个可插拔组件来处理,避免写在每个方法里。
02

DI 容器凭什么知道你缺什么★

decorator + emitDecoratorMetadata
容器不是"猜"的。TypeScript 的 emitDecoratorMetadata 会把 构造函数参数的类型写进元数据,容器读出来就知道该注入哪个类。

但interface 会被 TypeScript 抹掉,运行时不存在。 所以如果你依赖的是接口而不是类,就必须手动指定 token:

四种写法的使用场景 useClass:测试时换成 Mock 实现,或者按环境切换  |  最常用
useValue:配置对象、常量、已经创建好的第三方客户端
useFactory:需要异步初始化(连数据库、拉配置),依赖别的东西
useExisting:给同一个实例取别名,避免重复创建
面试这样答
Nest 的 DI 依赖两个东西:装饰器和反射元数据。开启 emitDecoratorMetadata 后,TypeScript 会把构造函数参数的类型写进 design:paramtypes 元数据,IoC 容器在实例化时读这个元数据,就知道要注入哪个类,然后递归地先把依赖创建好再 new 出来。因为接口在运行时会被擦除,所以依赖接口时要自定义 provider 并用字符串或 Symbol 作为 token,注入时用 @Inject 指定。自定义 provider 有 useClass、useValue、useFactory、useExisting 四种形式,分别对应替换实现、直接给值、异步初始化、取别名。
03

作用域:单例是默认,也是最优★

and why REQUEST bubbles up
默认是单例,整个应用只有一个实例。 改成 REQUEST 会让每个请求都新建一个, 而且这个作用域会沿着依赖链向上传染——依赖它的组件也会被迫变成 request 作用域。
作用域的传染(最容易踩) UserService 是单例,但它注入了 RequestContextService(REQUEST)
→ UserService 被迫也变成 REQUEST 作用域
→ 依赖 UserService 的 Controller 也变成 REQUEST
→ 整条链上每个请求都要重新实例化,性能明显下降

想拿请求上下文,更好的做法

用 AsyncLocalStorage(nestjs-cls 就是基于它)在中间件里存上下文,Service 仍然是单例,随时能取到当前请求的 id。

不要为了拿 req 就开 REQUEST

很多人只想要 req.user.id,就把整个 Service 设成 REQUEST,结果拖慢了整条依赖链。

面试这样答
Nest 的 provider 默认是单例,整个应用生命周期只创建一个实例,这是性能和资源管理最优的选择。另外还有 REQUEST 和 TRANSIENT 两种:REQUEST 是每个请求一个实例,适合需要请求级状态的场景;TRANSIENT 是每个注入点一个实例。要注意 REQUEST 作用域会向上传染——如果一个单例 service 注入了 request 作用域的 provider,它自己也会变成 request 作用域,整条依赖链上的组件都变成每请求重新实例化,性能影响很大。所以如果只是想拿请求上下文,更推荐用 AsyncLocalStorage 这类方案,保持 service 是单例。
04

模块是边界,不是文件夹

exports is the only door
模块划的是可注入范围。默认情况下,一个模块里的 provider 对其他模块完全不可见——想让别人用,必须在 exports 里明确写出来。
面试这样答
Nest 的模块是依赖注入的边界。一个模块里的 provider 默认只在本模块内可见,其他模块要用必须先 import 这个模块,而且对方还得在 exports 里明确导出。exports 里的 provider 才能被外部注入,没导出的即使 provide 了也是私有的。动态模块是通过静态方法返回 DynamicModule,把配置在 import 时传进去,常见的是 forRoot 做全局配置、forFeature 注册局部资源,还有 forRootAsync 用于依赖其他 provider 的异步初始化。
05

请求生命周期:一条链走完★

the single most-asked question
顺序是:Middleware → Guard → Interceptor(前置)→ Pipe → Controller → Interceptor(后置)→ Exception Filter。 注意 Pipe 在 Interceptor 之后——这个顺序很多人记错。
△ 可交互:跟着一个请求走一遍
1 Middleware Express 层。能改 req / res,但拿不到 Nest 的执行上下文
2 Guard 决定"能不能进"。返回 false 直接 403,后面的全都不执行
3 Interceptor(前置) 进入方法之前。可以改参数、记录开始时间
4 Pipe 转换 + 校验参数。校验失败抛 400,方法根本不会被调用
5 Controller → Service 真正执行业务逻辑
6 Interceptor(后置) 拿返回值做二次加工:包装格式、算耗时、写日志
7 Exception Filter 只在抛异常时介入。统一错误响应格式
点下面的按钮,看每一步发生什么。
面试这样答
请求进来先经过 Middleware,它是 Express 层的,只能操作 req 和 res。然后是 Guard,用 canActivate 决定这个请求有没有权限继续,返回 false 直接 403。接着是 Interceptor 的前置逻辑,然后才是 Pipe 做参数转换和校验,校验不过抛 400,控制器方法不会被调用。方法执行完,Interceptor 的后置逻辑处理返回值。整个过程中任何一步抛异常,都会跳到 Exception Filter 统一处理。要特别注意 Pipe 是在 Interceptor 之后执行的,所以 Guard 里拿不到校验后的 DTO。
06

Middleware:唯一还在 Express 层的

the odd one out
五个组件里,只有中间件是在 Nest 之外的。 它跑在 Express 的中间件栈上,拿不到 ExecutionContext, 也就无法知道"接下来要执行哪个控制器的哪个方法"。

适合放在中间件

  • 请求日志、请求 ID 注入
  • CORS、body parser、cookie parser
  • 限流(rate limit)
  • 挂载 AsyncLocalStorage 存上下文

不适合

  • 鉴权(用 Guard,它能拿到路由和 handler 元数据)
  • 参数校验(用 Pipe)
  • 响应格式统一(用 Interceptor)
面试这样答
Nest 的中间件实际上就是 Express 中间件,它运行在 Nest 路由处理之前,所以只能拿到 req、res、next,拿不到 Nest 的 ExecutionContext,也就不知道最终会命中哪个控制器方法。适合做请求日志、请求 ID 注入、CORS、限流这些不需要路由信息的处理。需要用模块的 configure 方法配合 MiddlewareConsumer 注册,可以排除特定路由。鉴权这类需要知道"要访问哪个 handler"的逻辑应该用 Guard 而不是中间件。
07

Guard:只回答"能不能"

boolean, not transformation
守卫的职责非常单一:能不能继续。返回 false 或抛异常就中断。 不要在里面做参数处理或响应加工,那是 Pipe 和 Interceptor 的事。
面试这样答
Guard 的职责是决定请求能不能继续,通过 canActivate 返回布尔值或 Promise、Observable,也可以直接抛异常。它比中间件强的地方是能拿到 ExecutionContext,可以访问当前要执行的 handler 和 controller 类,配合 Reflector 读出装饰器上写好的元数据,所以适合做基于角色的鉴权。要注意语义区分:未登录返回 401,已登录但权限不够返回 403。因为 Guard 在 Pipe 之前执行,所以拿不到经过校验和转换的 DTO。
08

Interceptor:方法前后的切面

rxjs pipelines around the handler
拦截器能同时插手"进入前"和"返回后",因为它拿到的是 next.handle() 这个 Observable——你可以在它之前做事, 也可以 pipe 它来改结果。
map 和 tap 的区别(常被追问) map:转换数据,返回值会被当作新的响应
tap:只做副作用(日志、埋点),不影响数据流
如果只想要副作用却用了 map 又忘了 return,响应会变成 undefined

典型用途

  • 统一响应包装 { code, data, message }
  • 接口耗时统计、慢接口告警
  • 简单缓存(配合 Reflector 读缓存 key)
  • 超时控制 timeout(5000)

别做的事

  • 在这里做鉴权(用 Guard,语义更清楚)
  • 在这里校验参数(用 Pipe,它拿不到解析后的 DTO)
  • 写复杂的业务逻辑(切面里藏业务最难查)
面试这样答
拦截器基于 RxJS,它拿到 next.handle() 这个 Observable,所以能在处理方法执行前后都插手:在返回它之前可以做前置处理,pipe 上去可以改返回值、做副作用或捕获异常。典型用途是统一响应格式、耗时统计、超时控制、简单缓存。用 map 会改变返回值,tap 只做副作用不影响数据流。要注意全局拦截器应该用 APP_INTERCEPTOR 这个 token 注册,直接用 useGlobalInterceptors 实例化的话就拿不到依赖注入。
09

Pipe 和 DTO:把校验挪出业务代码

transform + validate
管道做两件事:转换(字符串 → 数字、JSON → DTO 实例)和 校验(不合法直接抛 400)。 好处是控制器方法拿到的一定是干净、类型正确的数据。
whitelist 为什么重要 请求体:{ "email": "a@b.com", "role": "user", "isAdmin": true }
开了 whitelist 后,isAdmin 会被剥掉
如果直接把 req.body 传给 ORM 的 create,这个字段就能越权写入
面试这样答
管道负责两件事:转换和校验。转换是把请求里的字符串或普通对象变成目标类型或 DTO 实例,校验是用 class-validator 检查字段是否合法,不合法直接抛 400,控制器方法根本不会执行。DTO 必须写成 class 而不是 interface,因为装饰器和运行时反射需要真实存在的类。配置 ValidationPipe 时建议开 whitelist 剥离未声明字段、transform 转成类实例;开了 whitelist 能防止用户提交额外字段越权写入,这是个常见的安全点。全局管道用 APP_PIPE 注册,比 useGlobalPipes 更好,因为能使用依赖注入。
10

Exception Filter:最后一道处理

one shape for all errors
任何阶段抛出的异常都会汇到过滤器。它的作用是 把各种异常统一成一种响应格式,顺便保证不把堆栈泄露给用户。
内置异常(记住这几个状态码对应关系) 400 BadRequestException  ·  401 UnauthorizedException  ·  403 ForbiddenException
404 NotFoundException  ·  409 ConflictException  ·  500 InternalServerErrorException
面试这样答
异常过滤器是 Nest 异常处理的最后一环,请求链路上任何位置抛出的异常都会汇到这里。用 @Catch() 可以捕获所有异常,也可以指定只捕获某几类。它的核心作用是把不同来源的异常统一成一致的响应格式,同时做安全隔离——业务异常可以返回具体信息,非预期的错误只对外返回通用的 500,完整堆栈写进日志。注册时用 APP_FILTER 这个 token 能保留依赖注入。要避免的是捕获后静默返回成功,那样会掩盖真实故障。
11

装饰器:Nest 的语法基础

decorator writes metadata, reflector reads it
Nest 的所有"声明式"能力都是一个套路: 装饰器往类或方法上写元数据,运行时用 Reflector 读出来。 @Get()、@Roles()、@Public() 全是这个模式。

这套模式的三个组成部分

  • 写入:@SetMetadata(key, value)
  • 读取:Reflector.get / getAllAndOverride
  • 消费:在 Guard / Interceptor / Pipe 里用读到的值做判断

常见误区

  • 以为装饰器有"执行顺序"——它只在类定义时执行一次,不是每次请求
  • 在装饰器里直接写业务判断(那时没有请求上下文)
  • 忘了 emitDecoratorMetadata,导致 DI 和参数装饰器失效
面试这样答
Nest 的声明式能力都基于装饰器加反射元数据这个模式:装饰器在类定义时执行,往类或方法上写元数据;运行时守卫、拦截器通过 Reflector 把元数据读出来决定行为。比如 @Roles('admin') 本质是 SetMetadata,RolesGuard 里用 reflector.getAllAndOverride 读,方法级优先于类级。自定义参数装饰器用 createParamDecorator,返回值会作为参数注入。可以用 applyDecorators 把多个装饰器组合成一个语义更清楚的,比如把鉴权和角色检查合成 @Auth('admin')。
12

循环依赖:forwardRef 与它背后的设计问题

a symptom, not a feature
forwardRef 能解决循环依赖,但它只是让双向引用延迟求值。 真正该做的是问一句:这两个模块是不是职责划错了。
为什么需要 forwardRef 模块加载是自上而下的。UsersModule 加载时要用 OrdersModule,但 OrdersModule 还没定义完
→ 直接引用拿到 undefined
→ forwardRef 包成箭头函数,把"取值"推迟到两个模块都加载完之后
面试这样答
循环依赖的产生是因为模块或 provider 互相引用,而 Node 的模块加载是自上而下的,先加载的那个会拿到 undefined。forwardRef 的作用是把引用包成箭头函数,延迟到两个模块都加载完成后再求值,从而打破这个循环。要注意模块级和 provider 级都要分别加 forwardRef,只加一处还是会报错。但在工程上,循环依赖往往说明职责划分有问题,更好的做法是抽出共享模块,或者用事件机制解耦,而不是一味地加 forwardRef 把问题压下去。
13

配置、事务、那些容易写错的地方

config validation and transaction scope
配置要在启动时校验,缺环境变量就起不来,而不是跑到某个接口才炸。 事务要覆盖完整的业务边界,并且事务内所有操作必须用同一个 manager。
为什么要在启动时校验 启动失败:部署时立刻发现,回滚成本低
运行时才失败:某个低频接口调用时才 500,可能几天后才发现
这是"fail fast"原则最典型的应用
面试这样答
配置用 ConfigModule 加载,并且一定要配 validationSchema,在启动时校验必需的环境变量,缺失就直接启动失败,这比运行时才暴露问题好得多。事务方面,TypeORM 里可以用 dataSource.transaction 或者手动用 QueryRunner,关键是事务内的所有数据库操作必须使用事务提供的 manager,如果混用了普通的 repository,那部分操作会跑在事务之外,回滚时不会生效。QueryRunner 要在 finally 里 release 归还连接。另外不要在事务里发起外部 HTTP 请求,事务持有锁的时间会被拉长。
14

微服务:换传输层,不换代码

transport is a config detail
Nest 的微服务本质是"把 Controller 的入口从 HTTP 换成别的传输层"。 业务代码完全不动,只换装饰器和启动方式。

可选的 Transport

  • Transport.TCP —— 默认,简单直连
  • Transport.REDIS —— 用 Redis 做消息中转
  • Transport.RABBITMQ / KAFKA —— 需要可靠投递、削峰
  • Transport.GRPC —— 强类型、跨语言
  • Transport.NATS —— 轻量、云原生

两个必须分清的概念

MessagePattern 是请求-响应,调用方 send() 并等结果,本质是 RPC。
EventPattern 是事件广播,调用方 emit() 后立刻返回,没有返回值。
选错了会导致"以为发出去了其实没人收"或者"等一个永远不会来的响应"。

面试这样答
Nest 的微服务模块把入口从 HTTP 换成了其他传输层,但 Controller 和 Service 的代码结构不变,只是把 @Get/@Post 换成 @MessagePattern 或 @EventPattern,启动时用 connectMicroservice 指定 transport。它支持 TCP、Redis、RabbitMQ、Kafka、gRPC、NATS 等。要分清两种模式:MessagePattern 是请求-响应,调用方 send 并等待结果;EventPattern 是事件广播,emit 之后立即返回。一个应用可以同时开 HTTP 和微服务两个入口,共享同一套业务逻辑,这也是 Nest 分层做得好的体现。
15

Nest 里的 SSR:两种完全不同的路子★

mvc templates, or be the backend of one
Nest 本身不是 Next 那种元框架。它的 SSR 能力是 MVC 模板渲染——服务端拼 HTML,浏览器直接显示,没有 hydration。 要做 React/Vue 的 SSR,正确姿势是让 Nest 当 API 层,SSR 交给 Next/Nuxt。

路子一:MVC 模板渲染(Nest 自带)

这条路的特点 优点:实现简单,首屏直出、SEO 友好,不需要维护两套构建
限制:没有 hydration,交互要靠整页刷新或自己写少量 JS;不适合交互复杂的前端应用
所以它适合:后台管理、内容站、邮件模板、内部工具

路子二:Nest 做 API,SSR 交给前端框架(推荐)

浏览器请求访问 /posts
→
Next / Nuxt服务端发起数据请求
→
Nest API返回 JSON(业务、鉴权、数据都在这)
→
前端渲染 HTML返回带内容的页面
→
hydrate客户端接管交互

为什么这个分工更好?因为 Nest 的全部优势(模块化、DI、守卫、管道、微服务) 都在"业务和 API 层",而 SSR 需要的是"路由、数据预取、水合"这些前端框架的专长。 硬要让 Nest 干前端的活,等于放弃两边的长处。

选 MVC 模板渲染

页面交互少、要 SEO、团队只有后端、想快速上线。一个 Nest 服务全包,部署最简单。

别用它做复杂前端

需要大量客户端状态、路由动画、表单联动的场景。模板渲染会让交互逻辑变得极其难维护,这时候该换 Next/Nuxt。

面试这样答
Nest 的 SSR 有两条路。一是内置的 MVC 模式,通过 setViewEngine 配模板引擎,控制器用 @Render 指定模板并返回数据对象,框架把它渲染成完整 HTML 返回,适合后台管理、内容站这类交互不复杂的场景,但它没有 hydration,交互还是靠整页刷新。二是把 Nest 定位成 API 层或 BFF,SSR 交给 Next、Nuxt 这类前端框架,Nest 只负责业务、鉴权和数据。对需要 React 或 Vue 的服务端渲染的项目,第二种更合适,因为路由、数据预取、水合这些是元框架的专长,硬塞进 Nest 会两边都做不好。分离部署还带来一个好处:SSR 层吃 CPU,可以独立扩容。
16

速答清单

last 30 minutes before the interview

考前扫一遍。如果只有五分钟,优先看第 5 题。

QNest 和 Express 的本质区别?
Express 是"给你一个中间件管道,怎么组织随你";Nest 在它之上加了一层架构约定——模块划分、依赖注入、可插拔组件。所以 Nest 更适合多人协作的中大型项目,Express 更适合小服务或需要极致自由度的场景。
Q依赖注入是怎么实现的?
靠装饰器加反射元数据。开启 emitDecoratorMetadata 后,构造函数参数类型被写入 design:paramtypes,容器读出来递归解析依赖树,全部创建好再 new。接口因为运行时不存在,要用字符串或 Symbol 作为 token 加 @Inject。
Q四种自定义 provider 分别用在哪?
useClass 替换实现(测试 Mock、按环境切换);useValue 给现成的值(配置、常量、客户端实例);useFactory 需要异步初始化或依赖别的 provider;useExisting 给同一实例取别名,避免重复创建。
Qprovider 作用域怎么选?
默认单例,性能和资源管理最优,能用就用。REQUEST 是每请求一个实例,但会沿着依赖链向上传染,导致整条链都变成每请求实例化,性能影响大。要拿请求上下文优先用 AsyncLocalStorage 而不是开 REQUEST。
Q请求生命周期的完整顺序?
Middleware → Guard → Interceptor 前置 → Pipe → Controller/Service → Interceptor 后置 → Exception Filter。任何一步抛异常都直接跳到 Filter。特别注意 Pipe 在 Interceptor 之后,所以 Guard 拿不到校验后的 DTO。
Q中间件和守卫的区别?
中间件跑在 Express 层,只有 req/res/next,不知道最终命中哪个 handler,适合日志、CORS、限流。守卫能拿到 ExecutionContext,可以访问 handler 和 controller 类,配合 Reflector 读元数据,适合做鉴权。鉴权放中间件是常见错误。
Q拦截器里 map 和 tap 有什么区别?
map 会转换数据,它的返回值成为新的响应;tap 只做副作用,不影响数据流。只想打日志却用了 map 又忘了 return,响应会变成 undefined——这是很常见的手误。
QDTO 为什么必须是 class 不能是 interface?
interface 在编译后会被完全擦除,运行时不存在,没法挂 class-validator 的装饰器元数据,也没法被 class-transformer 转成实例。而 ValidationPipe 的校验和转换都依赖运行时的类信息。
QValidationPipe 的 whitelist 有什么用?
剥离 DTO 里没声明的字段。如果没有它,用户提交多余字段(比如 isAdmin: true)可能被直接传给 ORM 写入,造成越权。加上 forbidNonWhitelisted 更严格,发现多余字段直接报错。
Q模块的 exports 是干什么的?
控制 provider 的可见性。默认 provider 只在本模块内可注入,其他模块必须先 import 这个模块,且对方在 exports 里明确导出才行。这是 Nest 做依赖边界的方式。
Q循环依赖怎么处理?
用 forwardRef 把引用包成箭头函数延迟求值,模块级和 provider 级都要加。但循环依赖通常说明职责划分有问题,更好的做法是抽共享模块或者用事件解耦。
QMessagePattern 和 EventPattern 的区别?
前者是请求-响应,调用方 send 并等待返回结果,本质是 RPC;后者是事件广播,调用方 emit 后立即返回,没有返回值,可能有多个消费者。
QNest 怎么做 SSR?
两种。一是内置 MVC:setViewEngine 配模板引擎,控制器用 @Render 返回数据对象,服务端直接渲染成完整 HTML,但没有 hydration。二是 Nest 只做 API/BFF,SSR 交给 Next/Nuxt。要 React 或 Vue 的 SSR,第二种更合适。