NestJS 核心概念与工程实践详解

Posted by Qz on June 1, 2026

“Yeah It’s on. ”

正文

一、NestJS 是什么

官方文档:https://docs.nestjs.com/

Nest(NestJS)是一个用于构建高效、可扩展 Node.js 服务端应用的框架。它默认基于 Express,也可以切换为 Fastify;上层通过 TypeScript、装饰器、依赖注入、模块化和 AOP 风格的请求管线,给 Node.js 后端项目提供一套更接近企业级工程的应用架构。

一句话概括:Express / Koa 更像轻量 Web 库,NestJS 更像带工程架构约束的服务端应用框架

核心定位

NestJS 解决的不是“如何监听 HTTP 请求”这个问题,而是更上层的问题:

  • 如何组织大型后端项目的模块边界;
  • 如何管理服务之间的依赖关系;
  • 如何统一校验、鉴权、日志、异常处理、响应包装等横切逻辑;
  • 如何在 HTTP、WebSocket、微服务、GraphQL 等多种传输层之间复用业务代码;
  • 如何让后端项目更容易测试、扩展和维护。

官方文档对 Nest 的描述是:

Nest is a framework for building efficient, scalable Node.js server-side applications.

它结合了:

  • OOP:类、继承、接口、抽象、封装;
  • FP:函数式组合、管道、拦截器中的流式处理;
  • FRP:基于 RxJS 的 Observable 支持;
  • Angular 风格:模块、装饰器、依赖注入、生命周期。

可以这样理解:

你的业务代码
Controller / Service / Module / Provider
        ↓
NestJS 应用架构层
DI / Metadata / Pipe / Guard / Interceptor / Filter
        ↓
HTTP Adapter
ExpressAdapter 或 FastifyAdapter
        ↓
Express 或 Fastify
        ↓
Node.js HTTP Server

层级 作用 NestJS 负责模块化、依赖注入、装饰器、请求管线、异常处理 Express / Fastify 负责底层 HTTP 路由、请求响应、监听端口 Node.js http 负责真正的网络连接和 HTTP 协议处理

二、快速开始与项目结构

快速开始

安装 CLI:

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

最小启动入口:

import { NestFactory } from '@nestjs/core';
import { AppModule } from './app.module';

async function bootstrap() {
  const app = await NestFactory.create(AppModule);
  await app.listen(3000);
}
bootstrap();

NestFactory.create(AppModule) 是整个应用的启动入口。Nest 会从根模块 AppModule 开始扫描模块图、实例化依赖、注册路由,最后交给底层 HTTP 适配器监听端口。

基本目录结构

一个典型 Nest 项目大概是这样:

src
├── main.ts
├── app.module.ts
├── app.controller.ts
├── app.service.ts
└── users
    ├── users.module.ts
    ├── users.controller.ts
    ├── users.service.ts
    ├── dto
    │   ├── create-user.dto.ts
    │   └── update-user.dto.ts
    └── entities
        └── user.entity.ts

常见分层:

文件 作用
main.ts 应用启动入口
*.module.ts 模块定义,组织 controllers、providers、imports、exports
*.controller.ts 接收请求、定义路由、处理入参和响应
*.service.ts 业务逻辑,通常作为 Provider 被注入
dto 请求/响应数据结构,常配合校验管道使用
entity 数据库实体或领域模型

DTO 和 Entity 的区别

DTOData Transfer Object,也就是数据传输对象;Entity 是实体,通常表示数据库表结构或领域模型。

对比项 DTO Entity
关注点 接口入参 / 出参结构 数据库表结构 / 领域对象
使用位置 ControllerPipe、接口边界 ServiceRepository、ORM
是否对应数据库 不一定 通常对应数据库表
主要作用 限制、校验、转换外部输入 描述数据如何存储
常见装饰器 class-validator@IsString()@IsEmail() TypeORM 的 @Entity()@Column()
是否暴露给前端 可以作为请求 / 响应契约 不建议直接暴露完整 Entity

简单理解:DTO 管接口传什么,Entity 管数据库存什么

比如创建用户时,前端只需要传创建用户需要的字段:

export class CreateUserDto {
  name: string;
  email: string;
  password: string;
}

而数据库里的用户实体可能包含更多持久化相关字段:

@Entity()
export class User {
  @PrimaryGeneratedColumn()
  id: number;

  @Column()
  name: string;

  @Column()
  email: string;

  @Column()
  passwordHash: string;

  @CreateDateColumn()
  createdAt: Date;
}

不要直接把 Entity 当成 DTO 使用,否则可能导致:

  • 前端传入不该传的字段,例如 role: 'admin'
  • 响应中泄露敏感字段,例如 passwordHash
  • 数据库结构变化影响接口契约。

典型流程是:

请求 Body
  ↓
CreateUserDto 校验入参
  ↓
Service 处理业务逻辑
  ↓
User Entity 保存到数据库
  ↓
返回 Response DTO / 普通对象给前端

三、核心组成

Controller:路由控制器

Controller 负责处理请求和返回响应。Nest 使用装饰器把类和方法映射到路由。

import { Body, Controller, Delete, Get, Param, Post, Put, Query } from '@nestjs/common';
import { UsersService } from './users.service';
import { CreateUserDto } from './dto/create-user.dto';
import { UpdateUserDto } from './dto/update-user.dto';

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

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

  @Get()
  findAll(@Query('keyword') keyword?: string) {
    return this.usersService.findAll(keyword);
  }

  @Get(':id')
  findOne(@Param('id') id: string) {
    return this.usersService.findOne(id);
  }

  @Put(':id')
  update(@Param('id') id: string, @Body() dto: UpdateUserDto) {
    return this.usersService.update(id, dto);
  }

  @Delete(':id')
  remove(@Param('id') id: string) {
    return this.usersService.remove(id);
  }
}

常用装饰器:

装饰器 说明
@Controller('users') 声明控制器和路由前缀
@Get() / @Post() / @Put() / @Delete() 声明 HTTP 方法
@Param() 获取路径参数
@Query() 获取查询参数
@Body() 获取请求体
@Headers() 获取请求头
@Req() / @Res() 获取底层平台的 request / response 对象

Nest 推荐使用标准返回方式:Controller 方法直接 return 数据,框架负责序列化、设置状态码、发送响应。只有在确实需要底层能力时才使用 @Res(),否则会降低平台无关性,也可能绕过拦截器等 Nest 特性。

Provider 与 Service

Provider 是 Nest 依赖注入系统可以管理的对象。最常见的 Provider 是 Service。

import { Injectable } from '@nestjs/common';
import { CreateUserDto } from './dto/create-user.dto';
import { UpdateUserDto } from './dto/update-user.dto';

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

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

  findAll(keyword?: string) {
    if (!keyword) return this.users;
    return this.users.filter(user => user.name.includes(keyword));
  }

  findOne(id: string) {
    return this.users.find(user => user.id === id);
  }

  update(id: string, dto: UpdateUserDto) {
    const user = this.findOne(id);
    Object.assign(user, dto);
    return user;
  }

  remove(id: string) {
    const index = this.users.findIndex(user => user.id === id);
    if (index >= 0) this.users.splice(index, 1);
    return { success: true };
  }
}

@Injectable() 的核心作用是:告诉 Nest 这个类可以被 IoC 容器创建和注入。

Controller 中通过构造函数声明依赖:

constructor(private readonly usersService: UsersService) {}

这不是 Controller 主动 new UsersService(),而是 Nest 容器根据类型元数据自动解析并注入实例。

Module:模块系统

Module 是 Nest 应用的组织单元。每个应用至少有一个根模块。

import { Module } from '@nestjs/common';
import { UsersController } from './users.controller';
import { UsersService } from './users.service';

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

@Module() 的核心字段:

字段 说明
imports 导入其他模块,获得其导出的 Provider
controllers 当前模块的控制器
providers 当前模块内部注册的 Provider
exports 对外暴露的 Provider 或模块

模块默认是封装的:一个 Provider 只有在当前模块注册,或者从被导入模块的 exports 中导出,才能被当前模块注入。

根模块示例:

import { Module } from '@nestjs/common';
import { UsersModule } from './users/users.module';

@Module({
  imports: [UsersModule],
})
export class AppModule {}

动态模块

动态模块用于在运行时根据配置创建模块和 Provider,常见于数据库模块、配置模块、SDK 模块。

import { DynamicModule, Module } from '@nestjs/common';

@Module({})
export class DatabaseModule {
  static forRoot(options): DynamicModule {
    return {
      module: DatabaseModule,
      providers: [
        {
          provide: 'DATABASE_OPTIONS',
          useValue: options,
        },
        DatabaseService,
      ],
      exports: [DatabaseService],
    };
  }
}

使用:

@Module({
  imports: [
    DatabaseModule.forRoot({
      host: 'localhost',
      port: 3306,
    }),
  ],
})
export class AppModule {}

这也是 Nest 生态里大量 Module.forRoot()Module.forRootAsync() API 的来源。

依赖注入机制

Nest 的依赖注入能力主要来自三部分:

  1. TypeScript 装饰器;
  2. reflect-metadata 保存的运行时类型元数据;
  3. Nest 自己实现的 IoC 容器。

例如:

@Injectable()
export class AService {
  constructor(private readonly bService: BService) {}
}

当开启 emitDecoratorMetadata 后,TypeScript 会为构造函数参数生成类型元数据。Nest 在实例化 AService 时读取这些元数据,知道它依赖 BService,然后从容器里查找 BService 的实例。

自定义 Provider:

const redisProvider = {
  provide: 'REDIS_CLIENT',
  useFactory: async () => {
    const client = createRedisClient();
    await client.connect();
    return client;
  },
};

注入:

@Injectable()
export class CacheService {
  constructor(@Inject('REDIS_CLIENT') private readonly redisClient) {}
}

Provider 常见形式:

写法 场景
useClass 根据 token 使用某个类实现
useValue 注入常量、配置、mock 对象
useFactory 根据工厂函数动态创建实例
useExisting 给已有 Provider 创建别名

四、请求处理流程与 AOP 组件

请求生命周期

一次 HTTP 请求进入 Nest 后,大体顺序如下:

Request
  ↓
Middleware
  ↓
Guards
  ↓
Interceptors before
  ↓
Pipes
  ↓
Controller
  ↓
Service
  ↓
Interceptors after
  ↓
Response

异常流程:

Exception
  ↓
Exception Filters
  ↓
Error Response

不同组件职责:

组件 执行阶段 主要用途
Middleware 最早 日志、请求预处理、挂载上下文
Guard Controller 前 鉴权、权限判断、访问控制
Interceptor Controller 前后 日志、缓存、响应包装、超时、流转换
Pipe Controller 参数进入前 参数转换、DTO 校验
Controller 业务入口 路由处理
Service Controller 内部调用 业务逻辑
Exception Filter 异常时 统一异常响应

Middleware

Middleware 更接近 Express / Koa 中间件,适合处理底层请求预处理。

import { Injectable, NestMiddleware } from '@nestjs/common';

@Injectable()
export class LoggerMiddleware implements NestMiddleware {
  use(req, res, next: () => void) {
    console.log(`${req.method} ${req.originalUrl}`);
    next();
  }
}

注册:

import { MiddlewareConsumer, Module, NestModule } from '@nestjs/common';

@Module({})
export class AppModule implements NestModule {
  configure(consumer: MiddlewareConsumer) {
    consumer.apply(LoggerMiddleware).forRoutes('*');
  }
}

Pipe:参数转换和校验

Pipe 常用于 DTO 校验、类型转换、参数规范化。

DTO:

import { IsInt, IsString, Min } from 'class-validator';

export class CreateUserDto {
  @IsString()
  name: string;

  @IsInt()
  @Min(0)
  age: number;
}

全局启用校验:

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

参数级 Pipe:

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

Guard:鉴权和权限控制

Guard 决定请求是否可以继续执行。

import { CanActivate, ExecutionContext, Injectable } from '@nestjs/common';

@Injectable()
export class AuthGuard implements CanActivate {
  canActivate(context: ExecutionContext): boolean {
    const request = context.switchToHttp().getRequest();
    return Boolean(request.headers.authorization);
  }
}

使用:

@UseGuards(AuthGuard)
@Get('profile')
profile() {
  return { username: 'qz' };
}

Interceptor:拦截器

Interceptor 是 Nest 中非常重要的 AOP 扩展点。它可以包裹 Controller 方法,在执行前后统一处理逻辑。

import { CallHandler, ExecutionContext, Injectable, NestInterceptor } from '@nestjs/common';
import { map, Observable } from 'rxjs';

@Injectable()
export class ResponseInterceptor implements NestInterceptor {
  intercept(context: ExecutionContext, next: CallHandler): Observable<any> {
    return next.handle().pipe(
      map(data => ({
        code: 0,
        message: 'success',
        data,
      })),
    );
  }
}

全局注册:

app.useGlobalInterceptors(new ResponseInterceptor());

Exception Filter:异常过滤器

异常过滤器用于统一异常格式。

import { ArgumentsHost, Catch, ExceptionFilter, HttpException } from '@nestjs/common';

@Catch(HttpException)
export class HttpExceptionFilter implements ExceptionFilter {
  catch(exception: HttpException, host: ArgumentsHost) {
    const ctx = host.switchToHttp();
    const response = ctx.getResponse();
    const request = ctx.getRequest();
    const status = exception.getStatus();

    response.status(status).json({
      code: status,
      message: exception.message,
      path: request.url,
      timestamp: new Date().toISOString(),
    });
  }
}

生命周期钩子

Nest 支持模块和应用级生命周期事件。

常见钩子:

钩子 时机
onModuleInit() 当前模块依赖初始化完成后
onApplicationBootstrap() 所有模块初始化完成后,开始监听前
onModuleDestroy() 模块销毁时
beforeApplicationShutdown() 应用关闭前
onApplicationShutdown() 应用关闭时

示例:

import { Injectable, OnModuleInit, OnModuleDestroy } from '@nestjs/common';

@Injectable()
export class RedisService implements OnModuleInit, OnModuleDestroy {
  async onModuleInit() {
    await this.connect();
  }

  async onModuleDestroy() {
    await this.disconnect();
  }
}

如果需要监听系统信号:

app.enableShutdownHooks();

五、架构原理与源码分析

NestJS 架构原理

Nest 的核心架构可以理解为:

Decorators Metadata
        ↓
DependenciesScanner 扫描模块图
        ↓
NestContainer 保存模块、Provider、Controller
        ↓
InstanceLoader / Injector 实例化依赖
        ↓
RouterExplorer 扫描 Controller 路由
        ↓
HTTP Adapter 绑定到底层 Express / Fastify
        ↓
请求进入 Middleware / Guard / Pipe / Interceptor / Filter 管线

也就是说,Nest 不是替代 Express / Fastify 的底层 HTTP 能力,而是在它们之上增加:

  • 模块系统;
  • DI 容器;
  • 装饰器元数据;
  • 请求执行上下文;
  • 跨平台适配器;
  • AOP 风格扩展点。

源码分析:启动流程

源码入口:@nestjs/core/nest-factory.ts

典型调用:

const app = await NestFactory.create(AppModule);

内部核心流程大概是:

NestFactory.create(AppModule)
  ↓
创建 ApplicationConfig
  ↓
创建 NestContainer
  ↓
创建 GraphInspector
  ↓
创建 HTTP Adapter,默认 ExpressAdapter
  ↓
initialize()
  ↓
创建 Injector
  ↓
创建 InstanceLoader
  ↓
创建 DependenciesScanner
  ↓
dependenciesScanner.scan(AppModule)
  ↓
instanceLoader.createInstancesOfDependencies()
  ↓
dependenciesScanner.applyApplicationProviders()
  ↓
new NestApplication(...)

核心点:

  • NestFactory 是启动门面,负责组装容器、扫描器、注入器和应用实例;
  • create() 默认创建 Express adapter,如果传入 Fastify adapter,则切换到底层 Fastify;
  • initialize() 中真正完成模块扫描和依赖实例化;
  • 初始化完成后返回 NestApplication,再通过 app.listen() 启动 HTTP 服务。

源码分析:模块扫描

源码入口:@nestjs/core/scanner.ts

核心类:DependenciesScanner

scan() 的主要工作:

registerCoreModule()
  ↓
scanForModules()
  ↓
scanModulesForDependencies()
  ↓
addScopedEnhancersMetadata()
  ↓
calculateModulesDistance()
  ↓
container.bindGlobalScope()

模块扫描时,Nest 会读取 @Module() 装饰器写入的元数据:

  • imports
  • providers
  • controllers
  • exports

核心方法:

方法 作用
scanForModules() 递归扫描模块树,处理 imports 和动态模块
reflectImports() 读取并注册导入模块
reflectProviders() 读取并注册 Provider
reflectControllers() 读取并注册 Controller
reflectExports() 读取并注册导出项
reflectInjectables() 扫描 Guard、Interceptor、Pipe、Filter 等增强器

这说明 Nest 的模块系统本质上是:装饰器元数据 + 递归扫描 + 容器注册

源码分析:容器结构

源码入口:@nestjs/core/injector/container.ts

核心类:NestContainer

它主要保存:

NestContainer
├── modules: ModulesContainer
├── dynamicModulesMetadata: Map
├── globalModules: Set
├── internalProvidersStorage
└── moduleCompiler

重要职责:

方法 作用
addModule() 编译模块 token,并把模块加入容器
addProvider() 把 Provider 加入对应模块
addController() 把 Controller 加入对应模块
addImport() 建立模块之间的导入关系
addExportedProviderOrModule() 记录模块导出项
addDynamicMetadata() 保存动态模块元数据
bindGlobalScope() 把全局模块绑定到其他模块

在容器里,每个模块通常会持有:

  • providers
  • controllers
  • imports
  • exports
  • injectables
  • middlewares

所以 Nest 的依赖解析并不是全局随便找,而是沿着模块的 providersimportsexports 关系查找。

源码分析:依赖实例化

源码入口:@nestjs/core/injector/injector.ts

核心类:Injector

核心流程:

loadProvider()
  ↓
loadInstance()
  ↓
resolveConstructorParams()
  ↓
resolveSingleParam()
  ↓
resolveComponentWrapper()
  ↓
lookupComponent()
  ↓
lookupComponentInImports()
  ↓
instantiateClass()

实例化一个 Provider 时,Nest 会:

  1. 读取构造函数参数类型;
  2. 根据 token 在当前模块的 providers 中查找;
  3. 如果当前模块没有,沿着 imports 查找被导出的 Provider;
  4. 先递归实例化依赖;
  5. 再调用构造函数创建当前实例;
  6. 处理属性注入;
  7. 根据作用域缓存实例。

伪代码可以理解为:

function createInstance(ClassRef) {
  const deps = Reflect.getMetadata('design:paramtypes', ClassRef);
  const args = deps.map(dep => container.get(dep));
  return new ClassRef(...args);
}

实际源码比这个复杂得多,因为还要处理:

  • useFactory 异步工厂;
  • useValueuseClassuseExisting
  • @Inject() 自定义 token;
  • @Optional() 可选依赖;
  • forwardRef() 循环依赖;
  • 单例、请求级、瞬态作用域;
  • 请求上下文 ContextId
  • 属性注入;
  • 全局增强器。

源码分析:路由注册

源码入口:@nestjs/core/router/router-explorer.ts

核心类:RouterExplorer

核心流程:

RouterExplorer.explore()
  ↓
PathsExplorer.scanForPaths(controllerInstance)
  ↓
读取 @Controller() 和 @Get() / @Post() 等元数据
  ↓
创建 RouterExecutionContext
  ↓
创建异常代理 RouterProxy
  ↓
通过 RouterMethodFactory 获取 adapter.get/post/put/delete
  ↓
routerMethodRef(path, routeHandler)

它会把 Controller 方法转换成底层平台可识别的路由处理函数。

例如:

@Controller('users')
export class UsersController {
  @Get(':id')
  findOne() {}
}

最终会被转换为类似:

expressRouter.get('/users/:id', wrappedHandler);

这里的 wrappedHandler 不是原始 findOne,而是被 Nest 包装后的执行函数,里面包含:

  • Guard 执行;
  • Pipe 参数处理;
  • Interceptor 洋葱模型;
  • Exception Filter 异常处理;
  • 请求作用域 Provider 创建;
  • 响应结果处理。

源码分析:装饰器本质

以 Controller 为例,装饰器本身并不直接注册路由,它只是写入元数据。

可以简单理解为:

function Controller(path: string): ClassDecorator {
  return target => {
    Reflect.defineMetadata('path', path, target);
  };
}

function Get(path = ''): MethodDecorator {
  return (target, key, descriptor) => {
    Reflect.defineMetadata('method', 'GET', descriptor.value);
    Reflect.defineMetadata('path', path, descriptor.value);
  };
}

真正的注册动作发生在启动阶段:Nest 扫描这些元数据,然后调用底层 adapter 注册路由。

所以 Nest 的核心设计是:

编写阶段:装饰器声明意图
启动阶段:扫描元数据构建应用图
运行阶段:根据应用图处理请求

源码分析:HTTP Adapter

Nest 默认基于 Express,但可以切换 Fastify:

import { NestFactory } from '@nestjs/core';
import { FastifyAdapter, NestFastifyApplication } from '@nestjs/platform-fastify';

const app = await NestFactory.create<NestFastifyApplication>(
  AppModule,
  new FastifyAdapter(),
);
await app.listen(3000);

Adapter 层屏蔽底层框架差异。Nest 上层调用的是抽象接口,例如:

  • get()
  • post()
  • listen()
  • reply()
  • status()
  • setHeader()

底层分别由 ExpressAdapter 或 FastifyAdapter 实现。

这带来的好处是:业务代码可以尽量保持 Nest 风格,而不是强绑定 Express API。

六、与其他 Node.js 服务端框架对比

总览对比

框架 定位 风格 架构约束 TypeScript 性能 适合场景
Express 轻量 Web 框架 中间件 一般 中等 小型 API、快速原型
Koa 轻量中间件框架 洋葱模型 一般 中等 自定义框架、轻量服务
Fastify 高性能 Web 框架 插件 + Schema 中等 较好 高性能 API、网关
Hapi 配置驱动框架 插件 + 配置 较强 一般 中等 企业 API、配置化服务
Egg.js 企业级 Node 框架 约定优于配置 中等 中等 阿里系风格企业应用
Midway 企业级 TypeScript 框架 IoC + 装饰器 中等 国内 TS 后端、Serverless
NestJS 企业级应用框架 Module + DI + Decorator 取决于 Adapter 大型后端、微服务、团队协作

Express 和 Fastify 对比

Express 和 Fastify 都是 Node.js HTTP 框架,但定位不同:Express 更经典、更灵活,Fastify 更强调性能、Schema 和插件体系。

对比项 Express Fastify
定位 经典轻量 Web 框架 高性能 Web 框架
生态 历史久,中间件生态非常丰富 生态较新,但官方插件体系更规范
性能 中等,足够满足大多数业务场景 更高,适合高并发 API 场景
写法 中间件模型,简单直接 插件 + Schema 模型,约束更强
校验与序列化 通常依赖第三方中间件 内置 Schema 校验和高性能序列化能力
TypeScript 体验 可用,但不是从 TS 出发设计 类型支持更现代,配合 Schema 更自然
学习成本 低,上手快 略高,需要理解插件和 Schema
适合场景 普通 API、后台服务、快速原型 高性能 API、网关、对吞吐量敏感的服务

简单理解:

  • Express 更像一个自由度很高的基础 Web 工具箱;
  • Fastify 更像一个性能优先、带更多工程约束的 Web 框架。

在 NestJS 中,默认使用 Express:

const app = await NestFactory.create(AppModule);

如果要切换为 Fastify,需要显式传入 FastifyAdapter

const app = await NestFactory.create<NestFastifyApplication>(
  AppModule,
  new FastifyAdapter(),
);

选择建议:

  • 项目更看重生态兼容、团队熟悉度、第三方中间件,优先 Express;
  • 项目更看重性能、吞吐量、Schema 校验和序列化能力,可以考虑 Fastify;
  • 在 NestJS 中,只要业务代码不强依赖 @Res()@Req() 里的底层平台 API,Express 和 Fastify 的切换成本通常较低。

NestJS vs Express

Express 是 Node.js 生态最经典的 Web 框架。

Express 示例:

const express = require('express');
const app = express();

app.get('/users/:id', async (req, res) => {
  const user = await userService.findOne(req.params.id);
  res.json(user);
});

app.listen(3000);

优点:

  • 简单、直接、生态大;
  • 学习成本低;
  • 非常适合小项目和快速原型。

不足:

  • 没有内置模块系统;
  • 没有官方 DI;
  • 大项目目录结构容易依赖团队约定;
  • 鉴权、校验、异常、日志需要自行组合;
  • TypeScript 支持不是框架设计核心。

Nest 相比 Express 的优势:

  • 内置模块化组织;
  • 内置依赖注入;
  • 内置 Guard / Pipe / Interceptor / Filter;
  • 更适合大型项目和多人协作;
  • 可以继续使用 Express 生态。

结论:

  • 小项目:Express 更轻;
  • 中大型项目:Nest 的工程约束更有价值。

NestJS vs Koa

Koa 是 Express 原团队设计的下一代中间件框架,核心是 async/await 洋葱模型。

Koa 示例:

const Koa = require('koa');
const app = new Koa();

app.use(async (ctx, next) => {
  console.log('before');
  await next();
  console.log('after');
});

app.use(async ctx => {
  ctx.body = 'hello';
});

Koa 的核心优势:

  • 内核非常小;
  • 中间件模型优雅;
  • 适合自己封装上层框架。

不足:

  • 本身只提供 Context 和中间件;
  • 路由、参数校验、DI、模块化都要额外选型;
  • 大型项目需要自己设计架构。

Nest 的 Interceptor 和请求生命周期也有洋葱模型思想,但 Nest 在 Koa 这类模型之上增加了完整的工程体系。

结论:

  • 想要极简和高度自由:Koa;
  • 想要完整企业级架构:Nest。

NestJS vs Fastify

Fastify 是高性能 Node.js Web 框架,特点是 Schema 驱动和插件系统。

Fastify 示例:

fastify.get('/users/:id', {
  schema: {
    params: {
      type: 'object',
      properties: {
        id: { type: 'string' },
      },
    },
  },
}, async (request, reply) => {
  return userService.findOne(request.params.id);
});

Fastify 优势:

  • 性能好;
  • JSON Schema 校验和序列化效率高;
  • 插件封装能力强;
  • 适合高性能 API、BFF、网关。

Nest 与 Fastify 不是完全竞争关系,因为 Nest 可以使用 Fastify 作为底层 adapter。

NestFactory.create(AppModule, new FastifyAdapter())

对比:

维度 Fastify NestJS + Fastify
性能 更直接,开销更低 有 Nest 抽象层,略有额外成本
架构 插件系统 模块 + DI + AOP
开发体验 偏底层 偏应用框架
适合 高性能轻框架 高性能 + 工程架构

结论:

  • 极致性能、少抽象:Fastify;
  • 想兼顾性能和架构:Nest + Fastify。

NestJS vs Hapi

Hapi 是偏企业级的 Node.js 框架,强调配置化路由、插件、安全默认值。

Hapi 优势:

  • 插件体系成熟;
  • 配置驱动明显;
  • 内置较多服务端能力。

Nest 优势:

  • TypeScript 和装饰器体验更现代;
  • DI 和模块系统更清晰;
  • 与 Angular 风格接近;
  • 在微服务、GraphQL、WebSocket 等方向集成更统一。

结论:如果团队喜欢配置化、插件化,可以考虑 Hapi;如果团队需要 TypeScript 优先、模块化、DI、AOP,Nest 更合适。

NestJS vs Egg.js

Egg.js 是国内企业级 Node 框架,强调约定优于配置。

Egg 的典型特点:

  • Controller / Service / Plugin / Middleware 目录约定;
  • 插件机制成熟;
  • 适合传统 MVC 服务端应用;
  • 在国内阿里系生态中使用较多。

Nest 相比 Egg:

  • 更 TypeScript-first;
  • 更依赖装饰器和 DI;
  • 模块边界更显式;
  • 更接近 Angular / Spring 的应用架构;
  • 国际生态和官方文档更活跃。

Egg 的优势是约定明确、上手传统;Nest 的优势是类型、模块和依赖管理更强。

NestJS vs Midway

Midway 与 Nest 类似,也强调 TypeScript、IoC、装饰器。

Midway 常见优势:

  • 国内生态更熟;
  • 对 Serverless、一体化场景支持较好;
  • 与 Egg / 阿里云生态结合较多。

Nest 常见优势:

  • 国际生态更大;
  • 官方文档和课程资料丰富;
  • 微服务、GraphQL、WebSocket、OpenAPI 等模块成熟;
  • 与 Express / Fastify 适配成熟。

如果项目强依赖国内云厂商或 Midway 生态,Midway 有优势;如果希望使用更通用的国际 Node.js 后端生态,Nest 更合适。

七、优劣势分析

NestJS 优势分析

1. 架构清晰,适合大型项目

Nest 通过 Module 划分边界,每个业务域可以独立成模块:

UserModule
OrderModule
PaymentModule
NotificationModule

模块之间通过 importsexports 显式建立依赖,避免所有服务都变成全局可见。

2. DI 降低耦合,方便测试

Controller 依赖抽象的 Service,而不是自己创建实例。

测试时可以替换 Provider:

const moduleRef = await Test.createTestingModule({
  controllers: [UsersController],
  providers: [
    {
      provide: UsersService,
      useValue: mockUsersService,
    },
  ],
}).compile();

这让单元测试更简单。

3. AOP 能力强

Guard、Pipe、Interceptor、Filter 把横切逻辑从业务代码中抽离出来。

例如:

  • 鉴权不写在每个 Controller 里,而是写 Guard;
  • 参数校验不写在业务逻辑里,而是写 DTO + Pipe;
  • 响应包装不写在每个接口里,而是写 Interceptor;
  • 异常格式不散落各处,而是写 Exception Filter。
4. TypeScript 体验好

Nest 从设计上就围绕 TypeScript:

  • DTO 类型明确;
  • Provider 注入有类型提示;
  • Controller 入参返回值可描述;
  • 装饰器元数据支撑 DI 和路由映射;
  • 与 class-validator、class-transformer 配合自然。
5. 平台适配能力强

Nest 不强绑定某个底层 HTTP 框架:

  • 默认 Express;
  • 可切换 Fastify;
  • 支持微服务传输层;
  • 支持 GraphQL;
  • 支持 WebSocket;
  • 支持 CLI 和测试工具。
6. 生态完整

Nest 官方和社区提供了大量集成:

  • @nestjs/config
  • @nestjs/typeorm
  • @nestjs/mongoose
  • @nestjs/graphql
  • @nestjs/websockets
  • @nestjs/microservices
  • @nestjs/swagger
  • @nestjs/testing

相比 Express/Koa 需要自己拼装,Nest 提供了更统一的工程体验。

NestJS 劣势和注意点

Nest 不是所有场景的最优选择。

1. 学习成本更高

初学者需要理解:

  • Decorator;
  • Module;
  • Provider;
  • DI;
  • Pipe / Guard / Interceptor / Filter;
  • 生命周期;
  • 作用域。

如果只是写几个简单接口,Nest 可能显得重。

2. 抽象层带来性能成本

Nest 会在底层 HTTP 框架之上增加路由包装、执行上下文、AOP 管线、DI 解析等逻辑。多数业务系统这个开销可以接受,但极致性能场景下,直接使用 Fastify 可能更合适。

3. 过度封装可能降低可读性

如果项目滥用全局模块、动态模块、复杂 Provider token、过深的装饰器组合,代码可能变得难追踪。

建议:

  • 模块边界清晰;
  • 少用全局模块;
  • Provider token 命名统一;
  • Interceptor / Guard / Pipe 职责单一;
  • 不要为了模式而模式。

八、适用场景与实战建议

适用场景

适合使用 NestJS:

  • 中大型后端系统;
  • BFF 层;
  • 多模块业务系统;
  • 需要统一鉴权、校验、日志、异常处理;
  • 团队使用 TypeScript;
  • 项目需要较强测试能力;
  • 未来可能扩展微服务、GraphQL、WebSocket。

不一定适合:

  • 只有几个简单接口的小脚本;
  • 极致性能优先且团队能接受底层开发;
  • 不想引入装饰器和 DI 的简单 Node 项目;
  • 团队主要写纯 JavaScript 且不使用 TypeScript。

实战建议

模块划分

按照业务域划分模块,而不是按照技术层随意堆放。

推荐:

users
orders
payments
notifications

不推荐所有 Controller 放一个目录、所有 Service 放一个目录,后期业务边界会变得不清晰。

Controller 保持薄

Controller 只负责:

  • 接收参数;
  • 调用 Service;
  • 返回结果。

业务逻辑放 Service。

DTO 明确输入边界

所有写接口尽量使用 DTO:

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

配合全局 ValidationPipe 做白名单和类型转换。

Provider 不要滥用请求作用域

默认 Provider 是单例。请求作用域会增加实例创建成本,只有在确实需要请求级上下文时使用。

适合请求作用域的场景:

  • 多租户上下文;
  • 请求链路追踪;
  • GraphQL request cache;
  • 当前用户上下文。
全局模块谨慎使用

@Global() 可以减少 imports,但也会让依赖关系变隐式。通用工具、配置、数据库连接可以考虑全局,业务模块不建议全局。

九、日志、监控与告警

后端服务上线后,能不能快速发现问题、定位问题、复盘问题,主要依赖三类能力:

  • 日志:回答“发生了什么”;
  • 指标:回答“系统现在是否健康”;
  • 告警:回答“什么时候需要人介入”。

这三件事不要混在一起。日志适合排障和审计,监控适合看趋势和健康度,告警应该尽量基于稳定的指标触发,而不是简单扫描日志。

推荐组合

一个比较实用的 NestJS 生产方案:

NestJS
  ├─ Pino: JSON 日志
  ├─ Prometheus: /metrics 指标
  ├─ Grafana: 看板
  ├─ Alertmanager: 告警通知
  └─ Sentry: 异常聚合

对应职责:

能力 推荐方案 作用
应用日志 Pino / nestjs-pino 输出结构化 JSON 日志
指标监控 Prometheus 采集 QPS、错误率、延迟、资源水位
可视化看板 Grafana 展示服务健康状态和趋势
告警通知 Alertmanager / Grafana Alerting 将异常状态推送到 Slack、飞书、企业微信等
异常聚合 Sentry 聚合未捕获异常、错误堆栈和上下文
链路追踪 OpenTelemetry + Jaeger / Tempo 排查跨服务调用和慢请求

日志:用 Pino 输出结构化日志

开发阶段用 console.log 可以帮助理解流程,但生产环境不建议直接使用。更推荐使用 nestjs-pino,把日志统一输出成 JSON,方便被 Loki、ELK、OpenSearch 等系统采集和检索。

安装:

npm install nestjs-pino pino-http

注册:

import { Module } from '@nestjs/common';
import { LoggerModule } from 'nestjs-pino';

@Module({
  imports: [
    LoggerModule.forRoot({
      pinoHttp: {
        level: process.env.NODE_ENV === 'production' ? 'info' : 'debug',
        redact: [
          'req.headers.authorization',
          'req.headers.cookie',
          'req.body.password',
          'req.body.token',
        ],
      },
    }),
  ],
})
export class AppModule {}

在业务代码中注入 Logger:

import { Injectable } from '@nestjs/common';
import { PinoLogger } from 'nestjs-pino';

@Injectable()
export class UsersService {
  constructor(private readonly logger: PinoLogger) {
    this.logger.setContext(UsersService.name);
  }

  async createUser() {
    this.logger.info({ action: 'create_user' }, 'creating user');
  }
}

好的日志应该有稳定字段,而不是只写一段字符串:

{
  "level": "info",
  "time": 1780300800000,
  "requestId": "6f3b7c9e",
  "userId": "10001",
  "module": "UsersService",
  "action": "create_user",
  "msg": "creating user"
}

这样才能按 requestIduserIdactionmodule 快速检索。

监控:暴露 /metrics 给 Prometheus

日志适合排查单个问题,指标适合判断整体健康状态。常见指标包括:

  • 请求量:QPS、接口调用次数;
  • 错误率:HTTP 5xx、业务失败数;
  • 延迟:平均耗时、P95、P99;
  • 资源:CPU、内存、事件循环延迟;
  • 依赖:数据库连接数、Redis 延迟、队列积压;
  • 业务:订单创建数、支付成功率、消息发送失败数。

可以在 NestJS 中暴露 /metrics,由 Prometheus 定时拉取:

import { Controller, Get, Header } from '@nestjs/common';
import { register } from 'prom-client';

@Controller('metrics')
export class MetricsController {
  @Get()
  @Header('Content-Type', register.contentType)
  async metrics() {
    return register.metrics();
  }
}

对于接口耗时,可以用 Interceptor 统一记录:

import {
  CallHandler,
  ExecutionContext,
  Injectable,
  NestInterceptor,
} from '@nestjs/common';
import { Observable, tap } from 'rxjs';

@Injectable()
export class MetricsInterceptor implements NestInterceptor {
  intercept(context: ExecutionContext, next: CallHandler): Observable<unknown> {
    const now = Date.now();
    const request = context.switchToHttp().getRequest();

    return next.handle().pipe(
      tap(() => {
        const duration = Date.now() - now;
        const route = request.route?.path ?? request.url;

        // 这里可以写入 Prometheus Histogram,例如 http_request_duration_ms
        console.log({
          method: request.method,
          route,
          duration,
        });
      }),
    );
  }
}

真实项目里不要只 console.log,而是把 duration 写入 Prometheus 的 Histogram,用于统计 P95 / P99 延迟。

告警:优先基于指标,而不是日志

告警的目标不是“有错误就通知人”,而是“需要人介入时再通知人”。否则告警太多,团队很快就会对告警失去敏感度。

适合告警的指标:

  • 5 分钟内 HTTP 5xx 错误率超过阈值;
  • P95 延迟持续高于阈值;
  • 服务实例不可用;
  • CPU / 内存持续过高;
  • 数据库连接池耗尽;
  • 队列消息积压持续增长;
  • 支付成功率、下单成功率等核心业务指标异常下降。

不建议一开始就对所有 Error 日志告警。日志告警适合作为兜底,例如:

  • 出现 fatal 日志;
  • 支付、订单、资金相关错误连续出现;
  • 某个关键任务连续失败;
  • 出现安全相关异常。

异常聚合:用 Sentry 补足错误上下文

Prometheus 能告诉你“错误率升高了”,日志能告诉你“某次请求发生了什么”,Sentry 更适合聚合异常堆栈,帮助判断:

  • 哪个版本开始出现错误;
  • 哪个接口触发最多;
  • 影响了多少用户;
  • 错误堆栈和上下文是什么;
  • 是否已经被修复。

一般可以在全局 Exception Filter 中接入 Sentry,对未处理异常进行上报,同时继续返回统一错误响应。

关键实践

生产环境建议遵守这些原则:

  • 不要直接使用 console.log,统一使用 Logger;
  • 日志输出 JSON,方便机器采集、过滤和聚合;
  • 每个请求生成 requestId,并贯穿日志、错误、链路追踪;
  • 日志中不要输出密码、Token、Cookie、身份证号等敏感信息;
  • 告警优先基于指标,例如错误率、延迟、可用性和资源水位;
  • 日志告警只做兜底,避免因为噪音太多导致告警失效;
  • 业务核心流程要有业务指标,例如下单成功率、支付成功率;
  • 看板要少而关键,优先展示服务健康、接口延迟、错误率和核心业务指标;
  • Sentry 负责异常聚合,Prometheus 负责指标趋势,日志系统负责细节排查。

简单总结:

排查单次问题:看日志
判断系统健康:看指标
发现需要介入的问题:靠告警
定位异常堆栈:看 Sentry
分析跨服务慢请求:看链路追踪

十、总结

NestJS 的核心价值不是“比 Express 多几个装饰器”,而是提供了一整套服务端应用架构:

Module 负责边界
Provider 负责能力
DI 负责组装
Controller 负责入口
Pipe 负责校验转换
Guard 负责访问控制
Interceptor 负责横切逻辑
Filter 负责异常处理
Adapter 负责屏蔽底层平台差异

如果项目规模较小,Express / Koa / Fastify 更轻;如果项目会持续增长、团队协作复杂、需要统一工程规范和可测试性,NestJS 的模块化、依赖注入和 AOP 管线会非常有优势。

参考资料: