人人都会AI编程

12.3 NestJS

更新时间:2026-07-10

前面介绍的 Express 和 Koa 代表着 Node.js 服务端框架的两个重要方向:Express 通过中间件机制提供了极大的灵活性,Koa 则进一步优化了异步流程控制。但它们都比较“轻量”,不强制任何代码组织方式——这意味着随着项目的增长,目录结构、依赖关系、横切关注点(日志、权限)等容易失控。NestJS 正是在这一背景下诞生的,它借鉴了 Angular 的模块化思想和 Spring 的依赖注入体系,为 TypeScript 项目提供了一套开箱即用的企业级架构范式。

12.3.1 核心理念:模块化、依赖注入与面向切面编程

NestJS 建立在三个核心设计原则之上:

  1. 模块化(Modularity):应用被组织成一个个功能内聚的模块(@Module),每个模块封装自己的控制器、服务、提供者,并可以导入其他模块。这种划分让大型项目可以按领域边界拆分,便于多人协作和维护。
  2. 依赖注入(Dependency Injection):NestJS 内置了一个强大的 IoC 容器,通过构造函数参数自动注入依赖实例。开发者只需声明依赖关系,框架负责管理生命周期和实例化顺序,代码耦合度大幅降低,单元测试时也易于 Mock。
  3. 面向切面编程(AOP):通过守卫(Guards)、拦截器(Interceptors)、管道(Pipes)和过滤器(Filters),把认证、日志、数据转换、异常处理等横切逻辑从业务代码中剥离出来,既保持核心服务的纯净,又能灵活组合复用。

这些概念对于写过 Angular 的开发者非常亲切,但实际上 NestJS 的底层是构建在 Express(或 Fastify)之上的,可以无缝使用整个 Node.js 生态的中间件和库,并非另起炉灶。

12.3.2 TypeScript 原生支持与工程化体验

NestJS 从设计之初就以 TypeScript 为第一语言,项目初始化通过 @nestjs/cli 即可生成带有严格类型配置的项目骨架。框架本身大量使用装饰器和泛型,提供清晰的类型约束。例如:

@Controller('users')
export class UsersController {
  constructor(private readonly usersService: UsersService) {}

  @Get(':id')
  findOne(@Param('id', ParseIntPipe) id: number): Promise<User> {
    return this.usersService.findOne(id);
  }
}

在这段代码中:

  • @Controller 装饰器定义路由前缀;@Get 声明 GET 方法。
  • @Param 结合 ParseIntPipe 自动将字符串参数转换为数字类型,如果转换失败会抛出内置异常,不需要在控制器内写判断逻辑。
  • 返回值的 Promise<User> 类型让前端或者同项目下的其他服务可以准确推断结构。

NestJS 的工程化不仅体现在类型上,cli 可以快速生成模块、控制器、服务、过滤器等代码片段,保持项目风格统一。此外,它内置了对 Jest 测试的支持,通过依赖注入系统可以轻松地对每个单元进行隔离测试。

12.3.3 核心组件详解:控制器、服务、模块、守卫、拦截器

一个典型的 NestJS 模块包含以下组件,它们分工明确:

控制器(Controller)

控制器负责处理 HTTP 请求,解析输入参数,调用对应的服务层方法,并返回响应。通过装饰器声明路由、方法、状态码、头部等,大大减少了样板代码。控制器本身应该尽量轻薄,不包含复杂业务逻辑。

@Controller('posts')
export class PostsController {
  constructor(private readonly postsService: PostsService) {}

  @Post()
  @HttpCode(201)
  create(@Body() createPostDto: CreatePostDto) {
    return this.postsService.create(createPostDto);
  }

  @Get()
  findAll(@Query('page') page: number, @Query('limit') limit: number) {
    return this.postsService.findAll({ page, limit });
  }
}

服务(Service)

服务类使用 @Injectable() 装饰器标记,使得它可以被注入到控制器或其他服务中。服务层集中编写业务逻辑、数据库操作、外部 API 调用等,是真正的业务核心。因为它就是普通的类,可以独立测试。

@Injectable()
export class PostsService {
  constructor(
    @InjectRepository(Post)
    private readonly postRepository: Repository<Post>,
  ) {}

  async create(dto: CreatePostDto): Promise<Post> {
    const post = this.postRepository.create(dto);
    return this.postRepository.save(post);
  }

  async findAll(pagination: PaginationDto): Promise<Post[]> {
    return this.postRepository.find({
      skip: (pagination.page - 1) * pagination.limit,
      take: pagination.limit,
    });
  }
}

如果使用的是 TypeORM 或 Prisma,@InjectRepository 会动态注入对应实体的 Repository,服务完全不需要关心连接池的建立和销毁。

模块(Module)

每个模块通过 @Module 装饰器声明其包含的控制器、服务,以及需要导入的外部模块和对外暴露的服务。根模块(AppModule)是应用的入口,之后可以按功能拆分为用户模块、订单模块、文件模块等。

@Module({
  imports: [TypeOrmModule.forFeature([Post])],
  controllers: [PostsController],
  providers: [PostsService],
  exports: [PostsService],   // 如果其他模块需要
})
export class PostsModule {}

守卫(Guard)

守卫实现 CanActivate 接口,通过 @Injectable() 标记后,可以用 @UseGuards() 注解在控制器或具体路由上。典型的用例是权限认证:从请求中提取 Token,验证用户身份,决定是否允许访问该路由。

@Injectable()
export class AuthGuard implements CanActivate {
  constructor(private readonly jwtService: JwtService) {}

  canActivate(context: ExecutionContext): boolean {
    const request = context.switchToHttp().getRequest();
    const token = request.headers.authorization?.split(' ')[1];
    try {
      const payload = this.jwtService.verify(token);
      request.user = payload;
      return true;
    } catch {
      throw new UnauthorizedException('Token 无效');
    }
  }
}

在控制器中使用:

@UseGuards(AuthGuard)
@Get('profile')
getProfile(@Req() req) {
  return req.user;
}

拦截器(Interceptor)

拦截器可以在请求前后插入逻辑,常用于统一响应格式、日志记录、执行时间监测等。它实现 NestInterceptor 接口,通过 use() 方法包裹处理流程。

@Injectable()
export class TransformInterceptor<T> implements NestInterceptor<T, Response<T>> {
  intercept(context: ExecutionContext, next: CallHandler): Observable<Response<T>> {
    return next.handle().pipe(
      map(data => ({
        code: 200,
        data,
        timestamp: new Date().toISOString(),
      })),
    );
  }
}

全局应用拦截器后,所有接口的返回会被自动包装成统一的 JSON 结构,无需在每个控制器中重复处理。

管道(Pipe)

管道用于数据转换和校验,与控制器参数装饰器配合。NestJS 内置了 ValidationPipe,可与 class-validatorclass-transformer 联用,实现声明式的 DTO 校验。

export class CreateUserDto {
  @IsEmail()
  email: string;

  @MinLength(6)
  password: string;

  @IsOptional()
  @IsString()
  nickname?: string;
}

然后在全局启用验证管道:

app.useGlobalPipes(new ValidationPipe({ whitelist: true, transform: true }));

这样一旦请求体不合规,框架会直接返回 400 错误,并给出每个字段的具体校验失败原因。

异常过滤器(Exception Filter)

当业务抛出异常(如 NotFoundException)或未处理的错误时,异常过滤器可以捕获并统一格式化错误响应,比原生的 Express 错误处理更结构化和可控。

12.3.4 与 Express / Koa 的核心差异

虽然 NestJS 默认使用 Express 作为 HTTP 平台(也可以切换到 Fastify),但它和纯 Express/Koa 的编写方式截然不同:

| 对比维度 | Express / Koa | NestJS |
| ------------ | --------------------------------- | -------------------------------------------- |
| 架构约束 | 无约束,自由组织代码 | 强约束,模块/控制器/服务分层 |
| TypeScript | 手动配置,类型提示较弱 | 天生第一公民,泛型、装饰器深度集成 |
| 依赖注入 | 手动创建或第三方库 | 内置 IoC 容器,自动注入 |
| 横切逻辑管理 | 中间件顺序链式调用,缺乏分层概念 | 守卫/拦截器/管道/过滤器分工明确 |
| 测试友好度 | 需手动模拟请求或依赖桩 | 依赖注入使得 Mock 简单,测试工具链完整 |
| 开发效率 | 小项目起步快,但大型项目维护成本高 | 初期学习曲线略高,但长期维护成本低 |
| 适用场景 | 简单 API、中间件、微服务 | 中大型项目、复杂业务、团队协作场景 |

选择 NestJS 并不意味着 Express/Koa 的中间件完全失效。NestJS 提供了 app.use() 适配 Express 中间件,并且你依然可以在模块内部使用诸如 morgancors 等流行的中间件。因此,它并非替代品,而是一个更高层的抽象。

12.3.5 实际选型建议

  • 如果项目功能单一、接口数量少、团队人数少,Express/Koa 可以让你快速交付,过度的分层可能增加不必要的复杂度。
  • 如果业务逻辑中等复杂、需要清晰的测试策略、未来有扩展需求,NestJS 提供的模块化架构能有效控制代码腐化速度,是性价比很高的选择。
  • 如果是大型项目、多人协作、涉及多种客户端接入、需要复杂鉴权和审批流程,NestJS 的企业级能力(拦截器、守卫、自定义装饰器)能极大减少重复代码,值得前期投入学习成本。

许多团队在实践中的策略是:用 NestJS 搭建核心业务,对于一些独立的轻量级 API 网关或者 BFF 层,仍然用 Express 或 Fastify 应对,两者可以在微服务体系中共存。

12.3.6 快速上手实例:一个用户模块

下面展示一个最小的 NestJS 项目,涵盖模块、控制器、服务、DTO 和管道校验。

1. 创建项目

npm i -g @nestjs/cli
nest new project-name

2. 生成用户模块

nest g module users
nest g controller users
nest g service users

3. 定义 DTO(create-user.dto.ts)

import { IsEmail, MinLength } from 'class-validator';

export class CreateUserDto {
  @IsEmail()
  email: string;

  @MinLength(6)
  password: string;
}

4. 编写 Service(users.service.ts)

@Injectable()
export class UsersService {
  private users = [];

  create(dto: CreateUserDto) {
    const user = { id: Date.now(), ...dto };
    this.users.push(user);
    return user;
  }

  findAll() {
    return this.users;
  }
}

5. 编写 Controller(users.controller.ts)

@Controller('users')
export class UsersController {
  constructor(private readonly usersService: UsersService) {}

  @Post()
  create(@Body(new ValidationPipe()) dto: CreateUserDto) {
    return this.usersService.create(dto);
  }

  @Get()
  findAll() {
    return this.usersService.findAll();
  }
}

6. 注册模块(users.module.ts)

@Module({
  controllers: [UsersController],
  providers: [UsersService],
})
export class UsersModule {}

并在根模块 app.module.ts 中导入 UsersModule。启动后,访问 POST /users 提交不合规的 JSON 数据,会得到详细的 400 错误提示,无需额外代码。

12.3.7 总结

NestJS 通过 Angular 风格的架构封装,让 Node.js 后端开发拥有了类似于 Java Spring 的开发体验,但它依然保留了 Node.js 本身的性能和生态优势。对于追求代码可维护性、团队协作规范以及长期工程质量的项目,NestJS 是目前 Node.js 生态中最成熟的企业级框架选择之一。虽然入门时装饰器、模块、注入等概念会增加认知负担,但一旦掌握,开发效率和代码质量都将显著提升。