“Yeah It’s on. ”
正文
一、NestJS 是什么
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 的区别
DTO 是 Data Transfer Object,也就是数据传输对象;Entity 是实体,通常表示数据库表结构或领域模型。
| 对比项 | DTO | Entity |
|---|---|---|
| 关注点 | 接口入参 / 出参结构 | 数据库表结构 / 领域对象 |
| 使用位置 | Controller、Pipe、接口边界 |
Service、Repository、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 的依赖注入能力主要来自三部分:
- TypeScript 装饰器;
reflect-metadata保存的运行时类型元数据;- 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() 装饰器写入的元数据:
importsproviderscontrollersexports
核心方法:
| 方法 | 作用 |
|---|---|
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() |
把全局模块绑定到其他模块 |
在容器里,每个模块通常会持有:
providerscontrollersimportsexportsinjectablesmiddlewares
所以 Nest 的依赖解析并不是全局随便找,而是沿着模块的 providers、imports、exports 关系查找。
源码分析:依赖实例化
源码入口:@nestjs/core/injector/injector.ts
核心类:Injector
核心流程:
loadProvider()
↓
loadInstance()
↓
resolveConstructorParams()
↓
resolveSingleParam()
↓
resolveComponentWrapper()
↓
lookupComponent()
↓
lookupComponentInImports()
↓
instantiateClass()
实例化一个 Provider 时,Nest 会:
- 读取构造函数参数类型;
- 根据 token 在当前模块的
providers中查找; - 如果当前模块没有,沿着
imports查找被导出的 Provider; - 先递归实例化依赖;
- 再调用构造函数创建当前实例;
- 处理属性注入;
- 根据作用域缓存实例。
伪代码可以理解为:
function createInstance(ClassRef) {
const deps = Reflect.getMetadata('design:paramtypes', ClassRef);
const args = deps.map(dep => container.get(dep));
return new ClassRef(...args);
}
实际源码比这个复杂得多,因为还要处理:
useFactory异步工厂;useValue、useClass、useExisting;@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
模块之间通过 imports 和 exports 显式建立依赖,避免所有服务都变成全局可见。
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"
}
这样才能按 requestId、userId、action、module 快速检索。
监控:暴露 /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 管线会非常有优势。
参考资料:
- NestJS 官方文档
- NestJS Controllers
- NestJS Providers
- NestJS Modules
- NestJS Request lifecycle
- NestJS Core 源码