NestJS 中的 AOP:五种方式解耦通用逻辑
NestJS 里的一个非常实用的设计思想——AOP(面向切面编程)。如果你写过一些后端代码,肯定遇到过这样的情况:很多地方都需要做同样的事,比如日志记录、权限校验、参数验证、异常处理……如果把这些代码到处复制粘贴,不仅累,还容易出错。AOP 就是用来解决这个问题的,它让我们能把这类“横切关注点”从业务逻辑中抽离出来,统一管理和复用。
在 NestJS 里,AOP 的实现非常优雅,一共有五种方式:Middleware、Guard、Pipe、Interceptor、ExceptionFilter。它们分别在请求处理的不同阶段发挥作用,而且可以灵活地在全局、控制器或路由级别启用。今天我们就来一一拆解,看看它们都是干什么的、怎么用,以及它们之间的执行顺序是怎样的。
什么是 AOP?
先简单说一下 AOP 的概念。我们传统的 MVC 架构中,请求先进 Controller,然后调 Service,最后返回响应。但在这个过程中,总有一些逻辑是“横跨”很多地方的,比如:
- 打印请求日志
- 检查用户是否登录
- 验证参数格式
- 捕获异常并返回统一格式的错误信息
这些逻辑和核心业务没有直接关系,但又不得不写。AOP 的思想就是把这些“横切”的逻辑单独提取出来,形成一个个“切面”,然后在合适的地方“织入”到主流程中。这样一来,业务代码就干净了,切面逻辑也能复用,还能动态增删。
NestJS 中的五种 AOP 机制
NestJS 底层基于 Express,但它在 Express 的中间件基础上又封装了 Guard、Pipe、Interceptor、ExceptionFilter,让我们可以更精细地控制请求处理流程。下面我们挨个来看。
1. Middleware(中间件)
中间件是 Express 里的老熟人,Nest 也保留了它。中间件在请求进入路由之前执行,可以拿到请求和响应对象,也能调用 next() 把控制权交给下一个中间件或路由处理函数。
主要作用:通用的请求预处理,比如日志记录、请求体解析、跨域设置等。
如何实现:
需要实现 NestMiddleware 接口,写一个 use 方法:
import { Injectable, NestMiddleware } from '@nestjs/common';
import { Request, Response, NextFunction } from 'express';
@Injectable()
export class LoggerMiddleware implements NestMiddleware {
use(req: Request, res: Response, next: NextFunction) {
console.log('Request...', req.method, req.url);
next(); // 别忘了调用 next,否则请求会卡住
}
}如何启用:
在模块里实现 NestModule 接口的 configure 方法,指定哪些路由应用这个中间件。
import { Module, NestModule, MiddlewareConsumer } from '@nestjs/common';
@Module({})
export class AppModule implements NestModule {
configure(consumer: MiddlewareConsumer) {
consumer.apply(LoggerMiddleware).forRoutes('*'); // 所有路由
}
}也可以直接在 main.ts 里用 app.use() 注册全局中间件,但那样无法享受依赖注入。
2. Guard(守卫)
守卫顾名思义,就是守门员,它决定一个请求能不能继续往下走。通常用来做权限验证,比如检查用户是否登录、角色是否匹配。
主要作用:返回 true 放行,返回 false 拒绝请求(默认返回 403 禁止访问)。
如何实现:
实现 CanActivate 接口,写 canActivate 方法,返回布尔值或包含布尔值的 Promise/Observable。
import { Injectable, CanActivate, ExecutionContext } from '@nestjs/common';
import { Observable } from 'rxjs';
@Injectable()
export class AuthGuard implements CanActivate {
canActivate(
context: ExecutionContext,
): boolean | Promise<boolean> | Observable<boolean> {
const request = context.switchToHttp().getRequest();
// 这里做你的校验逻辑,比如检查 token
return !!request.headers.authorization;
}
}如何启用:
- 局部使用:在 Controller 或方法上用
@UseGuards(AuthGuard)装饰器。 全局使用:
- 在
AppModule的providers里用APP_GUARD令牌注册,这样守卫就在 IoC 容器里,可以注入其他服务。 - 在
main.ts里用app.useGlobalGuards(new AuthGuard()),但这种方式创建的实例不在容器内,无法注入依赖。
- 在
3. Pipe(管道)
管道有两个核心功能:参数转换 和 参数验证。它会在参数被传递给控制器之前执行。
主要作用:把字符串转成数字(比如 ParseIntPipe)、验证对象结构(配合 class-validator)、设置默认值等。
如何实现:
实现 PipeTransform 接口,写 transform 方法。
import { PipeTransform, Injectable, ArgumentMetadata, BadRequestException } from '@nestjs/common';
@Injectable()
export class ParseIntPipe implements PipeTransform<string, number> {
transform(value: string, metadata: ArgumentMetadata): number {
const val = parseInt(value, 10);
if (isNaN(val)) {
throw new BadRequestException('参数必须为数字');
}
return val;
}
}如何启用:
- 参数级别:
@Param('id', ParseIntPipe) id: number - 方法级别:
@UsePipes(SomePipe) - 控制器级别:同样用
@UsePipes() - 全局:
app.useGlobalPipes(new SomePipe())或通过APP_PIPE令牌注入。
Nest 内置了很多实用的管道,比如 ValidationPipe、ParseBoolPipe、ParseUUIDPipe 等,直接用就行。
4. Interceptor(拦截器)
拦截器是 AOP 中最灵活的一环。它可以在控制器方法执行前后添加逻辑,甚至可以完全改写返回值或抛出异常。它基于 RxJS,所以你能用 RxJS 的操作符来处理响应流。
主要作用:日志记录、响应映射(比如统一包装成 { data, code, message })、超时处理、缓存等。
如何实现:
实现 NestInterceptor 接口,写 intercept 方法。调用 next.handle() 就会触发控制器方法,返回的是一个 Observable,你可以用 RxJS 操作符对数据流进行各种操作。
import { Injectable, NestInterceptor, ExecutionContext, CallHandler } from '@nestjs/common';
import { Observable } from 'rxjs';
import { tap } from 'rxjs/operators';
@Injectable()
export class LoggingInterceptor implements NestInterceptor {
intercept(context: ExecutionContext, next: CallHandler): Observable<any> {
const now = Date.now();
return next.handle().pipe(
tap(() => console.log(`Request took ${Date.now() - now}ms`)),
);
}
}如何启用:
类似 Guard,可以用 @UseInterceptors(LoggingInterceptor) 局部启用,或通过 APP_INTERCEPTOR 令牌全局注册,或者在 main.ts 里 app.useGlobalInterceptors()。
拦截器 vs 中间件:
拦截器能拿到目标控制器和方法的元数据(通过 context.getClass() 和 context.getHandler()),还能结合自定义装饰器实现更精细的控制,这是中间件做不到的。所以如果逻辑和特定业务相关,用拦截器更合适;纯通用的请求预处理,中间件就够了。
5. ExceptionFilter(异常过滤器)
异常过滤器负责处理应用内抛出的所有异常(包括未捕获的),然后返回统一的错误响应。Nest 内置了很多 HTTP 异常类(如 BadRequestException、NotFoundException 等),但你可以自定义过滤器来完全控制响应格式。
主要作用:捕获异常,返回友好的 JSON 或页面,记录错误日志等。
如何实现:
实现 ExceptionFilter 接口,用 @Catch() 指定要捕获的异常类型,然后在 catch 方法里处理。
import { ExceptionFilter, Catch, ArgumentsHost, HttpException } from '@nestjs/common';
import { Request, Response } from 'express';
@Catch(HttpException) // 只捕获 HttpException 及其子类
export class HttpExceptionFilter implements ExceptionFilter {
catch(exception: HttpException, host: ArgumentsHost) {
const ctx = host.switchToHttp();
const response = ctx.getResponse<Response>();
const request = ctx.getRequest<Request>();
const status = exception.getStatus();
response.status(status).json({
statusCode: status,
timestamp: new Date().toISOString(),
path: request.url,
message: exception.message,
});
}
}如何启用:
- 方法级别:
@UseFilters(HttpExceptionFilter) - 控制器级别
- 全局:
app.useGlobalFilters(new HttpExceptionFilter())或通过APP_FILTER令牌注入。
五种机制的调用顺序
这么多切面,它们到底按什么顺序执行?其实 NestJS 有一套清晰的流程:
- 请求进来,先经过中间件(Middleware)。
- 到达路由后,依次执行守卫(Guard)——判断是否有权限。
- 通过守卫后,进入拦截器的
intercept方法前半部分(比如计时开始)。 - 然后管道(Pipe)对参数进行验证和转换。
- 参数准备好后,调用控制器方法。
- 控制器返回结果(可能是普通值或 Observable),然后回到拦截器的后半部分(比如计时结束、修改响应)。
- 如果整个过程中抛出异常,会被异常过滤器(ExceptionFilter)捕获并处理。
注意:守卫和管道都在拦截器的 next.handle() 之前执行,但守卫更靠前。中间件是最外层的,甚至可以在守卫之前。
总结
NestJS 提供的这五种 AOP 机制,覆盖了请求处理的整个生命周期,让我们可以把通用逻辑优雅地抽离出来。什么时候用哪个,可以这样简单记:
- 中间件:通用的、和业务无关的预处理(比如日志、CORS)。
- 守卫:权限校验,决定能不能访问。
- 管道:参数校验和转换,确保数据正确。
- 拦截器:对控制器前后进行细粒度扩展,可以操作响应流。
- 异常过滤器:全局异常处理,统一错误格式。

Comments | NOTHING