人人都会AI编程

控制器、服务、模块、守卫、拦截器体系

更新时间:2026-07-10

NestJS 是一个高度结构化的 Node.js 服务端框架,它的核心设计理念来自 Angular,强调模块化、依赖注入和面向切面编程(AOP)。理解这套体系并不需要死记硬背概念,关键是弄清楚:每个请求从进入系统到返回响应的完整生命周期中,这些组件分别在哪个阶段做什么事。

下面我们从最基础的“模块”开始,依次拆解各个角色的职责和协作方式。


1. 模块:组织代码的基本单元

每个 NestJS 应用至少有一个根模块(AppModule),然后功能被拆分到一个个特性模块中。模块负责将控制器、服务等组件打包成一个范围明确的功能块,并通过 providerscontrollers 数组声明该模块包含哪些可注入的对象。

@Module({
  imports: [DatabaseModule, CacheModule],     // 引入其他模块
  controllers: [UserController],               // 注册控制器
  providers: [UserService],                    // 注册服务
  exports: [UserService],                      // 导出给其他模块使用
})
export class UserModule {}

模块的实用原则:

  • 按业务域拆分:UserModuleOrderModuleAuthModule,而不是按技术层拆分。
  • 跨模块共享的服务必须显式 exports,否则注入会报错。
  • 全局模块可以用 @Global() 装饰,但应谨慎使用,避免依赖关系模糊。

2. 控制器:请求的入口

控制器负责接收 HTTP 请求,并返回响应。它是路由的载体,决定了哪个 URL 由哪个方法处理。控制器的职责应该很“薄”:只做参数提取、调用服务、构建响应对象,不应该包含业务逻辑

@Controller('users')
export class UserController {
  constructor(private readonly userService: UserService) {}

  @Get(':id')
  async findOne(@Param('id', ParseIntPipe) id: number) {
    return this.userService.findById(id);
  }

  @Post()
  async create(@Body() createUserDto: CreateUserDto) {
    return this.userService.create(createUserDto);
  }
}

关键点:

  • 使用 @Get@Post@Put@Delete 等装饰器映射 HTTP 动词和路径。
  • 通过管道(如 ParseIntPipe)进行参数转换和校验,控制器本身不应做校验。
  • 返回值会被 NestJS 自动序列化为 JSON,并设置合适的 Content-Type。

3. 服务:业务逻辑的载体

服务是真正干活的地方。它被注解为 @Injectable(),通过依赖注入被控制器或其他服务使用。服务包含了核心业务逻辑、数据库操作、外部 API 调用等。

@Injectable()
export class UserService {
  constructor(
    @InjectRepository(User)
    private readonly userRepository: Repository<User>,
  ) {}

  async findById(id: number): Promise<User> {
    const user = await this.userRepository.findOneBy({ id });
    if (!user) {
      throw new NotFoundException(`用户 ${id} 不存在`);
    }
    return user;
  }

  async create(dto: CreateUserDto): Promise<User> {
    const user = this.userRepository.create(dto);
    return this.userRepository.save(user);
  }
}

服务的设计要点:

  • 一个服务只负责一个明确的领域,避免“万能 Service”。
  • 业务逻辑集中在服务中,控制器只做委托调用。
  • 服务之间可以互相注入,但要避免循环依赖(可用 forwardRef 解决,但更推荐重构代码结构)。

4. 守卫:请求的“门卫”

守卫在请求到达控制器之前执行,用于判断该请求是否有权继续。最常见的场景是身份认证和权限校验。守卫返回 true 时请求继续,返回 false 或抛异常时请求被拦截。

@Injectable()
export class AuthGuard implements CanActivate {
  canActivate(context: ExecutionContext): boolean {
    const request = context.switchToHttp().getRequest();
    const token = request.headers.authorization;
    if (!token) {
      throw new UnauthorizedException('未提供认证令牌');
    }
    try {
      request.user = verifyToken(token);
      return true;
    } catch {
      throw new UnauthorizedException('令牌无效或已过期');
    }
  }
}

使用方式:

@Controller('admin')
@UseGuards(AuthGuard)        // 整个控制器受保护
export class AdminController { ... }

@Get('profile')
@UseGuards(AuthGuard, RoleGuard)  // 单一路由受保护,可以组合多个守卫
getProfile(@Req() req) {
  return req.user;
}

守卫的执行时机: 在所有中间件之后,但在任何管道和拦截器之前。


5. 拦截器:请求与响应的“包装器”

拦截器可以在方法执行前后介入,处理以下通用需求:

  • 在方法执行绑定额外逻辑(如观测计时)
  • 在方法执行转换响应结构(如统一包装成 { code, data, message } 格式)
  • 在异常时处理错误响应
  • 完全覆盖方法行为(如实现缓存)
@Injectable()
export class TransformInterceptor implements NestInterceptor {
  intercept(context: ExecutionContext, next: CallHandler): Observable<any> {
    const now = Date.now();
    return next.handle().pipe(
      map(data => ({
        code: 200,
        data,
        message: 'success',
        timestamp: new Date().toISOString(),
      })),
      tap(() => console.log(`请求耗时: ${Date.now() - now}ms`)),
    );
  }
}

全局应用:

app.useGlobalInterceptors(new TransformInterceptor());

或者在模块级别:

@Module({
  providers: [{
    provide: APP_INTERCEPTOR,
    useClass: TransformInterceptor,
  }],
})
export class AppModule {}

拦截器 vs 守卫 vs 中间件: 中间件在请求进入框架之前(路由解析之前)执行,适合处理如日志、CORS 等底层任务;守卫做权限判断;拦截器做方法前后的增强和转换。三层各有分工。


6. 请求生命周期的完整协作流程

以一个需要认证的 GET /users/:id 请求为例,完整流程如下:

  1. 中间件 先执行(如果有全局或模块中间件),处理请求日志、CORS 头等。
  2. 路由匹配,找到对应的控制器方法。
  3. 守卫 执行认证检查,失败则直接返回 401。
  4. 拦截器的 intercept 方法(前处理)执行,可以记录开始时间。
  5. 管道:id 进行类型转换和验证(如 ParseIntPipe)。
  6. 控制器 方法执行,调用 userService.findById(id)
  7. 服务 执行数据库查询,返回用户对象或抛出异常。
  8. 如果无异常,拦截器的 pipe 操作(后处理)转换响应体为统一格式。
  9. 框架将最终响应序列化为 JSON 发送给客户端。

在这个流程中,每个组件各司其职,开发者可以清晰地决定“验证”放在哪一层,“日志”放在哪一层,“变换响应”放在哪一层,从而让代码职责单一、可测试、可维护。


7. 体系设计的工程落地建议

  • 控制器要薄,服务要厚:业务复杂度上升时,重构服务比重构控制器安全得多。
  • 不要滥用拦截器:虽然能统一格式很方便,但把过多业务逻辑塞进拦截器会让调试变难。拦截器更适合切面关注点(日志、事务、缓存)。
  • 守卫 + 自定义装饰器 = 优雅的权限模型:比如用 @Roles('admin') 配合 RolesGuard,元数据(Reflector)驱动权限控制,比在每个方法里写判断清晰得多。
  • 模块拆分适度:对于小型项目,模块过多反而增加目录跳转成本;对于中型以上项目,按领域拆模块能有效界定边界,避免相互侵入。
  • 利用 Nest CLI 生成骨架nest g module usernest g controller usernest g service user 可以快速搭建一致的项目结构。

NestJS 的这套体系,本质上是对“分层架构”和“AOP”思想的具体实现。它不要求你刚入门就完全掌握所有概念,但随着业务规模的增长,这种有规矩、可插拔的结构会成为保持代码整洁的强有力保障。