跳到主要内容

NestJS 框架深入

问题

NestJS 是什么?它的核心架构和工作原理是怎样的?请求生命周期中各环节(Middleware、Guard、Interceptor、Pipe、ExceptionFilter)如何协作?

答案

NestJS 是一个基于 TypeScript 构建的渐进式 Node.js 服务端框架,采用 Angular 风格的装饰器驱动模块化架构,内置 IoC(控制反转)容器,提供开箱即用的企业级开发体验。它底层默认使用 Express 作为 HTTP 平台,也可切换为 Fastify。


1. NestJS 概述

核心理念

理念说明
Angular 风格借鉴 Angular 的模块、装饰器、依赖注入设计
装饰器驱动使用 @Module@Controller@Injectable 等装饰器声明式编程
模块化以 Module 为单位组织代码,每个 Module 自成一体
平台无关底层可切换 Express/Fastify,上层 API 保持一致
TypeScript 原生从诞生之初就以 TypeScript 为第一公民

与 Express/Koa/Fastify 的关系

NestJS 不是 Express 的替代品,而是站在 Express(或 Fastify)之上的架构层

对比Express/KoaNestJS
定位HTTP 库/框架企业级应用框架
架构自由组织强约束模块化
依赖注入无内置内置 IoC 容器
TypeScript手动配置原生支持
学习曲线中等偏高
适用场景小型项目、微服务中大型项目、企业应用
为什么选择 NestJS
  • 企业级架构:内置分层结构,团队协作更规范
  • 丰富的生态:官方提供 TypeORM、Mongoose、GraphQL、WebSocket、微服务等模块
  • 高可测试性:依赖注入天然支持 Mock 和单元测试
  • 适合构建 BFF 网关层、微服务、REST API、GraphQL 服务等

快速上手

npm install @nestjs/cli -g
nest new my-project

生成的项目结构:

src/
├── app.module.ts # 根模块
├── app.controller.ts # 根控制器
├── app.service.ts # 根服务
└── main.ts # 入口文件
src/main.ts
import { NestFactory } from '@nestjs/core';
import { AppModule } from './app.module';

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

2. 核心概念

NestJS 的三大核心构件是 ModuleControllerProvider

Module(模块)

模块是 NestJS 组织代码的基本单元,使用 @Module() 装饰器声明。

src/user/user.module.ts
import { Module } from '@nestjs/common';
import { UserController } from './user.controller';
import { UserService } from './user.service';
import { AuthModule } from '../auth/auth.module';

@Module({
imports: [AuthModule], // 导入其他模块
controllers: [UserController], // 注册控制器
providers: [UserService], // 注册提供者(服务)
exports: [UserService], // 导出给其他模块使用
})
export class UserModule {}
属性说明
imports导入本模块依赖的其他模块,可使用其 exports 中的 Provider
controllers本模块的控制器列表
providers本模块的提供者列表(Service、Repository 等)
exports将本模块的 Provider 导出给其他模块使用
全局模块

使用 @Global() 装饰器可将模块声明为全局模块,其他模块无需 imports 即可使用其导出的 Provider:

@Global()
@Module({
providers: [ConfigService],
exports: [ConfigService],
})
export class ConfigModule {}

Controller(控制器)

控制器负责处理 HTTP 请求,使用路由装饰器映射请求路径。

src/user/user.controller.ts
import {
Controller, Get, Post, Put, Delete,
Param, Body, Query, HttpCode, HttpStatus,
} from '@nestjs/common';
import { UserService } from './user.service';
import { CreateUserDto, UpdateUserDto } from './dto';

@Controller('users') // 路由前缀 /users
export class UserController {
constructor(private readonly userService: UserService) {}

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

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

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

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

@Delete(':id')
@HttpCode(HttpStatus.NO_CONTENT)
remove(@Param('id') id: string) {
return this.userService.remove(id);
}
}

常用参数装饰器:

装饰器说明等价 Express
@Param(key?)路由参数req.params / req.params[key]
@Body(key?)请求体req.body / req.body[key]
@Query(key?)查询参数req.query / req.query[key]
@Headers(name?)请求头req.headers / req.headers[name]
@Req()请求对象req
@Res()响应对象res

Provider / Service(提供者 / 服务)

Provider 是 NestJS 中最核心的概念 -- 任何可以被注入的类都是 Provider。最常见的 Provider 是 Service(服务层,负责业务逻辑)。

src/user/user.service.ts
import { Injectable, NotFoundException } from '@nestjs/common';
import { CreateUserDto, UpdateUserDto } from './dto';

interface User {
id: string;
name: string;
email: string;
}

@Injectable() // 标记为可注入的 Provider
export class UserService {
private users: User[] = [];

findAll(page: number = 1): User[] {
const pageSize = 10;
return this.users.slice((page - 1) * pageSize, page * pageSize);
}

findOne(id: string): User {
const user = this.users.find(u => u.id === id);
if (!user) {
throw new NotFoundException(`User #${id} not found`);
}
return user;
}

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

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

remove(id: string): void {
const index = this.users.findIndex(u => u.id === id);
if (index === -1) {
throw new NotFoundException(`User #${id} not found`);
}
this.users.splice(index, 1);
}
}
注意

@Injectable() 装饰器是必须的。没有它,NestJS 无法将该类纳入 IoC 容器管理。此外,Provider 必须在所属 Module 的 providers 数组中注册后才能被注入使用。


3. 依赖注入(IoC/DI)

依赖注入(Dependency Injection)是 NestJS 架构的基石。通过 IoC(Inversion of Control,控制反转)容器,类不再自己创建依赖,而是由容器自动注入。

IoC 容器原理

NestJS 启动时,IoC 容器会:

  1. 扫描所有 Module 中注册的 Provider
  2. 解析每个 Provider 构造函数的参数类型(通过 TypeScript 的 emitDecoratorMetadata 反射元数据)
  3. 构建依赖图并按正确顺序实例化
  4. 缓存实例(默认单例)

注入作用域

NestJS 提供三种注入作用域:

injection-scopes.ts
import { Injectable, Scope } from '@nestjs/common';

// 1. DEFAULT(默认)- 单例,整个应用共享一个实例
@Injectable()
export class SingletonService {}

// 2. REQUEST - 每个请求创建一个新实例
@Injectable({ scope: Scope.REQUEST })
export class RequestScopedService {}

// 3. TRANSIENT - 每次注入创建一个新实例
@Injectable({ scope: Scope.TRANSIENT })
export class TransientService {}
作用域实例化时机生命周期使用场景
DEFAULT应用启动时与应用相同(单例)无状态服务、工具类
REQUEST每个请求到来时请求结束后销毁需要请求上下文(如多租户)
TRANSIENT每次注入时消费者决定需要独立状态的服务
作用域冒泡

如果一个 DEFAULT 作用域的 Service 注入了一个 REQUEST 作用域的 Service,那么外层 Service 也会被提升为 REQUEST 作用域。这叫作用域冒泡,会影响性能,需谨慎使用。

自定义 Provider

NestJS 支持四种自定义 Provider 方式:

custom-providers.ts
import { Module } from '@nestjs/common';

// 1. useClass - 指定类(可动态替换实现)
const configProvider = {
provide: ConfigService,
useClass: process.env.NODE_ENV === 'test'
? MockConfigService
: ConfigService,
};

// 2. useValue - 注入常量/对象
const apiKeyProvider = {
provide: 'API_KEY',
useValue: 'my-secret-api-key',
};

// 3. useFactory - 工厂函数(支持异步)
const dbProvider = {
provide: 'DATABASE_CONNECTION',
useFactory: async (configService: ConfigService) => {
const dbConfig = configService.get('database');
return createConnection(dbConfig);
},
inject: [ConfigService], // 工厂函数的依赖
};

// 4. useExisting - 为已有 Provider 创建别名
const loggerAlias = {
provide: 'AliasLogger',
useExisting: LoggerService,
};

@Module({
providers: [
ConfigService,
LoggerService,
configProvider,
apiKeyProvider,
dbProvider,
loggerAlias,
],
})
export class AppModule {}

使用 @Inject() 注入自定义 Token:

使用自定义 Token 注入
import { Injectable, Inject } from '@nestjs/common';

@Injectable()
export class ApiService {
constructor(
@Inject('API_KEY') private readonly apiKey: string,
@Inject('DATABASE_CONNECTION') private readonly db: Connection,
) {}
}

循环依赖处理

当两个 Provider 互相依赖时,需要使用 forwardRef 打破循环:

circular-dependency.ts
import { Injectable, Inject, forwardRef } from '@nestjs/common';

@Injectable()
export class CatService {
constructor(
@Inject(forwardRef(() => DogService))
private readonly dogService: DogService,
) {}
}

@Injectable()
export class DogService {
constructor(
@Inject(forwardRef(() => CatService))
private readonly catService: CatService,
) {}
}
尽量避免循环依赖

循环依赖说明模块设计存在耦合问题。推荐的解决方式:

  1. 提取公共逻辑到第三个 Service
  2. 使用事件驱动(EventEmitter)解耦
  3. 重新审视模块边界

4. 请求生命周期

NestJS 拥有一套完整的请求管线,每个请求按固定顺序流经各环节。理解这条管线是掌握 NestJS 的关键。

各环节详细执行顺序:

记忆口诀

M-G-I-P-H-I-EMiddleware → Guard → Interceptor → Pipe → Handler → Interceptor → ExceptionFilter

环节职责典型用途
Middleware请求预处理日志、CORS、Cookie 解析
Guard访问控制(返回 boolean)认证、授权、RBAC
Interceptor请求/响应转换日志、缓存、响应包装、超时
Pipe数据验证与转换DTO 验证、参数类型转换
Handler业务处理控制器方法
ExceptionFilter异常处理统一错误格式、异常日志

5. 中间件(Middleware)

NestJS 中间件本质上就是 Express 中间件,在路由处理之前执行。

类中间件 vs 函数中间件

src/common/middleware/logger.middleware.ts
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) {
const start = Date.now();
res.on('finish', () => {
const duration = Date.now() - start;
console.log(`${req.method} ${req.url} ${res.statusCode} - ${duration}ms`);
});
next();
}
}

// 函数中间件 - 简单场景
export function corsMiddleware(req: Request, res: Response, next: NextFunction) {
res.setHeader('Access-Control-Allow-Origin', '*');
next();
}

注册中间件

src/app.module.ts
import { Module, NestModule, MiddlewareConsumer, RequestMethod } from '@nestjs/common';
import { LoggerMiddleware } from './common/middleware/logger.middleware';

@Module({ /* ... */ })
export class AppModule implements NestModule {
configure(consumer: MiddlewareConsumer) {
consumer
.apply(LoggerMiddleware)
.exclude(
{ path: 'health', method: RequestMethod.GET }, // 排除健康检查
)
.forRoutes('*'); // 应用到所有路由
}
}

全局中间件(在 main.ts 中注册):

src/main.ts
import helmet from 'helmet';
import compression from 'compression';

async function bootstrap() {
const app = await NestFactory.create(AppModule);
app.use(helmet()); // 安全头
app.use(compression()); // Gzip 压缩
await app.listen(3000);
}

6. 守卫(Guard)

Guard 专门负责访问控制,决定请求是否允许继续。与 Middleware 不同,Guard 可以访问 ExecutionContext,知道下一步将执行哪个 Handler。

基本结构

src/common/guards/auth.guard.ts
import {
Injectable, CanActivate, ExecutionContext,
UnauthorizedException,
} from '@nestjs/common';
import { Request } from 'express';

@Injectable()
export class AuthGuard implements CanActivate {
canActivate(context: ExecutionContext): boolean {
const request = context.switchToHttp().getRequest<Request>();
const token = request.headers.authorization?.replace('Bearer ', '');

if (!token) {
throw new UnauthorizedException('Missing token');
}

try {
const payload = this.verifyToken(token);
request['user'] = payload; // 将用户信息挂载到 request
return true;
} catch {
throw new UnauthorizedException('Invalid token');
}
}

private verifyToken(token: string): Record<string, unknown> {
// JWT 验证逻辑
return {};
}
}

角色守卫(RBAC)

结合自定义装饰器实现角色控制:

src/common/decorators/roles.decorator.ts
import { SetMetadata } from '@nestjs/common';

export const ROLES_KEY = 'roles';
export const Roles = (...roles: string[]) => SetMetadata(ROLES_KEY, roles);
src/common/guards/roles.guard.ts
import { Injectable, CanActivate, ExecutionContext } from '@nestjs/common';
import { Reflector } from '@nestjs/core';
import { ROLES_KEY } from '../decorators/roles.decorator';

@Injectable()
export class RolesGuard implements CanActivate {
constructor(private reflector: Reflector) {} // 注入反射器读取元数据

canActivate(context: ExecutionContext): boolean {
const requiredRoles = this.reflector.getAllAndOverride<string[]>(
ROLES_KEY,
[context.getHandler(), context.getClass()],
);

if (!requiredRoles) return true; // 未设置角色要求,放行

const { user } = context.switchToHttp().getRequest();
return requiredRoles.some(role => user.roles?.includes(role));
}
}
在控制器中使用
@Controller('admin')
@UseGuards(AuthGuard, RolesGuard)
export class AdminController {
@Roles('admin', 'super-admin')
@Get('dashboard')
getDashboard() {
return { message: 'Welcome, admin!' };
}
}

7. 拦截器(Interceptor)

拦截器在 Handler 前后都能执行逻辑,基于 RxJS 的 Observable 实现,功能强大且灵活。

响应包装拦截器

src/common/interceptors/transform.interceptor.ts
import {
Injectable, NestInterceptor, ExecutionContext, CallHandler,
} from '@nestjs/common';
import { Observable, map } from 'rxjs';

interface Response<T> {
code: number;
message: string;
data: T;
}

@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,
message: 'success',
data,
})),
);
}
}

日志拦截器

src/common/interceptors/logging.interceptor.ts
import {
Injectable, NestInterceptor, ExecutionContext, CallHandler, Logger,
} from '@nestjs/common';
import { Observable, tap } from 'rxjs';

@Injectable()
export class LoggingInterceptor implements NestInterceptor {
private readonly logger = new Logger(LoggingInterceptor.name);

intercept(context: ExecutionContext, next: CallHandler): Observable<unknown> {
const request = context.switchToHttp().getRequest();
const { method, url } = request;
const now = Date.now();

this.logger.log(`${method} ${url}`); // 前置:请求进入

return next.handle().pipe(
tap(() => {
this.logger.log(`${method} ${url} +${Date.now() - now}ms`); // 后置:请求完成
}),
);
}
}

超时拦截器

src/common/interceptors/timeout.interceptor.ts
import {
Injectable, NestInterceptor, ExecutionContext,
CallHandler, RequestTimeoutException,
} from '@nestjs/common';
import { Observable, timeout, catchError, throwError, TimeoutError } from 'rxjs';

@Injectable()
export class TimeoutInterceptor implements NestInterceptor {
intercept(context: ExecutionContext, next: CallHandler): Observable<unknown> {
return next.handle().pipe(
timeout(5000), // 5 秒超时
catchError(err => {
if (err instanceof TimeoutError) {
return throwError(() => new RequestTimeoutException());
}
return throwError(() => err);
}),
);
}
}

缓存拦截器

src/common/interceptors/cache.interceptor.ts
import {
Injectable, NestInterceptor, ExecutionContext, CallHandler,
} from '@nestjs/common';
import { Observable, of, tap } from 'rxjs';

@Injectable()
export class CacheInterceptor implements NestInterceptor {
private cache = new Map<string, { data: unknown; expiry: number }>();

intercept(context: ExecutionContext, next: CallHandler): Observable<unknown> {
const request = context.switchToHttp().getRequest();
const key = request.url;
const cached = this.cache.get(key);

if (cached && cached.expiry > Date.now()) {
return of(cached.data); // 命中缓存,直接返回
}

return next.handle().pipe(
tap(data => {
this.cache.set(key, {
data,
expiry: Date.now() + 60_000, // 缓存 60 秒
});
}),
);
}
}

8. 管道(Pipe)

Pipe 有两个典型用途:数据转换(将输入转为期望格式)和数据验证(验证输入是否合法)。

内置管道

NestJS 提供多个开箱即用的管道:

管道用途
ValidationPipeDTO 验证(配合 class-validator)
ParseIntPipe字符串转整数
ParseFloatPipe字符串转浮点数
ParseBoolPipe字符串转布尔值
ParseUUIDPipe验证 UUID 格式
ParseArrayPipe验证并转换数组
ParseEnumPipe验证枚举值
DefaultValuePipe设置默认值
使用内置管道
@Get(':id')
findOne(
@Param('id', ParseIntPipe) id: number, // 自动将 string 转为 number
) {
return this.userService.findOne(id);
}

@Get()
findAll(
@Query('page', new DefaultValuePipe(1), ParseIntPipe) page: number,
@Query('limit', new DefaultValuePipe(10), ParseIntPipe) limit: number,
) {
return this.userService.findAll(page, limit);
}

全局 ValidationPipe + class-validator

这是 NestJS 中最常用的验证方式:

npm install class-validator class-transformer
src/user/dto/create-user.dto.ts
import {
IsString, IsEmail, IsOptional,
MinLength, MaxLength, IsEnum,
} from 'class-validator';

enum UserRole {
ADMIN = 'admin',
USER = 'user',
}

export class CreateUserDto {
@IsString()
@MinLength(2)
@MaxLength(20)
name: string;

@IsEmail()
email: string;

@IsString()
@MinLength(8)
password: string;

@IsOptional()
@IsEnum(UserRole)
role?: UserRole;
}
src/main.ts - 全局注册 ValidationPipe
import { ValidationPipe } from '@nestjs/common';

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

app.useGlobalPipes(new ValidationPipe({
whitelist: true, // 自动剥离 DTO 中未定义的属性
forbidNonWhitelisted: true, // 存在未定义属性时报错
transform: true, // 自动将请求参数转为 DTO 实例
}));

await app.listen(3000);
}
whitelist 的重要性

开启 whitelist: true 后,客户端传入 DTO 中未声明的字段会被自动过滤,有效防止批量赋值攻击(Mass Assignment)。例如,用户注册时偷偷传入 role: 'admin' 会被自动剔除。

自定义管道

src/common/pipes/parse-date.pipe.ts
import { PipeTransform, Injectable, BadRequestException } from '@nestjs/common';

@Injectable()
export class ParseDatePipe implements PipeTransform<string, Date> {
transform(value: string): Date {
const date = new Date(value);
if (isNaN(date.getTime())) {
throw new BadRequestException(`"${value}" is not a valid date`);
}
return date;
}
}

// 使用
@Get('events')
findByDate(@Query('date', ParseDatePipe) date: Date) {
return this.eventService.findByDate(date);
}

9. 异常过滤器(ExceptionFilter)

NestJS 内置了统一的异常处理层。未被捕获的异常最终由 ExceptionFilter 处理,返回友好的错误响应。

内置异常类

NestJS 提供了一系列 HttpException 子类:

异常类HTTP 状态码
BadRequestException400
UnauthorizedException401
ForbiddenException403
NotFoundException404
MethodNotAllowedException405
ConflictException409
UnprocessableEntityException422
InternalServerErrorException500
ServiceUnavailableException503
抛出内置异常
import { NotFoundException, ConflictException } from '@nestjs/common';

@Injectable()
export class UserService {
async create(dto: CreateUserDto) {
const existing = await this.userRepo.findByEmail(dto.email);
if (existing) {
throw new ConflictException('Email already exists');
}
return this.userRepo.save(dto);
}

async findOne(id: string) {
const user = await this.userRepo.findById(id);
if (!user) {
throw new NotFoundException(`User #${id} not found`);
}
return user;
}
}

自定义全局异常过滤器

src/common/filters/all-exception.filter.ts
import {
ExceptionFilter, Catch, ArgumentsHost,
HttpException, HttpStatus, Logger,
} from '@nestjs/common';
import { Request, Response } from 'express';

@Catch() // 不传参数表示捕获所有异常
export class AllExceptionFilter implements ExceptionFilter {
private readonly logger = new Logger(AllExceptionFilter.name);

catch(exception: unknown, host: ArgumentsHost) {
const ctx = host.switchToHttp();
const request = ctx.getRequest<Request>();
const response = ctx.getResponse<Response>();

const status = exception instanceof HttpException
? exception.getStatus()
: HttpStatus.INTERNAL_SERVER_ERROR;

const message = exception instanceof HttpException
? exception.message
: 'Internal Server Error';

const errorResponse = {
code: status,
message,
timestamp: new Date().toISOString(),
path: request.url,
method: request.method,
};

// 记录错误日志
this.logger.error(
`${request.method} ${request.url} ${status}`,
exception instanceof Error ? exception.stack : undefined,
);

response.status(status).json(errorResponse);
}
}

注册全局异常过滤器:

src/main.ts
import { AllExceptionFilter } from './common/filters/all-exception.filter';

async function bootstrap() {
const app = await NestFactory.create(AppModule);
app.useGlobalFilters(new AllExceptionFilter());
await app.listen(3000);
}
通过 DI 注册全局 Filter

如果异常过滤器需要注入其他 Provider(如 Logger Service),应使用 Module 方式注册:

import { APP_FILTER } from '@nestjs/core';

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

同理,APP_GUARDAPP_INTERCEPTORAPP_PIPE 也支持这种注册方式。


10. 其他重要特性

定时任务(@nestjs/schedule)

src/tasks/tasks.service.ts
import { Injectable, Logger } from '@nestjs/common';
import { Cron, CronExpression, Interval, Timeout } from '@nestjs/schedule';

@Injectable()
export class TasksService {
private readonly logger = new Logger(TasksService.name);

@Cron(CronExpression.EVERY_DAY_AT_MIDNIGHT)
handleDailyCleanup() {
this.logger.log('每日清理任务执行中...');
}

@Interval(60_000)
handleHealthCheck() {
this.logger.log('每 60 秒健康检查...');
}

@Timeout(5000)
handleInit() {
this.logger.log('启动 5 秒后执行初始化...');
}
}

配置管理(@nestjs/config)

src/config/configuration.ts
export default () => ({
port: parseInt(process.env.PORT || '3000', 10),
database: {
host: process.env.DB_HOST || 'localhost',
port: parseInt(process.env.DB_PORT || '5432', 10),
name: process.env.DB_NAME || 'mydb',
},
jwt: {
secret: process.env.JWT_SECRET,
expiresIn: process.env.JWT_EXPIRES_IN || '1h',
},
});
src/app.module.ts
import { ConfigModule, ConfigService } from '@nestjs/config';
import configuration from './config/configuration';

@Module({
imports: [
ConfigModule.forRoot({
isGlobal: true, // 全局可用
load: [configuration], // 加载配置
envFilePath: ['.env.local', '.env'],
}),
],
})
export class AppModule {}
使用 ConfigService
@Injectable()
export class DatabaseService {
constructor(private configService: ConfigService) {}

getDbHost(): string {
return this.configService.get<string>('database.host', 'localhost');
}
}

限流(@nestjs/throttler)

src/app.module.ts
import { ThrottlerModule, ThrottlerGuard } from '@nestjs/throttler';
import { APP_GUARD } from '@nestjs/core';

@Module({
imports: [
ThrottlerModule.forRoot({
throttlers: [
{ ttl: 60_000, limit: 100 }, // 每分钟最多 100 次请求
],
}),
],
providers: [
{ provide: APP_GUARD, useClass: ThrottlerGuard },
],
})
export class AppModule {}

WebSocket 网关

src/events/events.gateway.ts
import {
WebSocketGateway, WebSocketServer,
SubscribeMessage, MessageBody,
ConnectedSocket, OnGatewayConnection, OnGatewayDisconnect,
} from '@nestjs/websockets';
import { Server, Socket } from 'socket.io';

@WebSocketGateway({ cors: true })
export class EventsGateway implements OnGatewayConnection, OnGatewayDisconnect {
@WebSocketServer()
server: Server;

handleConnection(client: Socket) {
console.log(`Client connected: ${client.id}`);
}

handleDisconnect(client: Socket) {
console.log(`Client disconnected: ${client.id}`);
}

@SubscribeMessage('message')
handleMessage(
@ConnectedSocket() client: Socket,
@MessageBody() payload: { text: string },
) {
this.server.emit('message', {
sender: client.id,
text: payload.text,
timestamp: Date.now(),
});
}
}

常见特性汇总

特性用途
定时任务@nestjs/scheduleCron/Interval/Timeout 定时任务
配置管理@nestjs/config环境变量、配置文件管理
缓存@nestjs/cache-manager内存/Redis 缓存
限流@nestjs/throttler请求频率限制
WebSocket@nestjs/websockets实时通信
GraphQL@nestjs/graphqlGraphQL API
微服务@nestjs/microservicesTCP/Redis/NATS/gRPC 微服务
TypeORM@nestjs/typeorm数据库 ORM 集成
Mongoose@nestjs/mongooseMongoDB ODM 集成
Swagger@nestjs/swaggerAPI 文档自动生成
健康检查@nestjs/terminus应用健康检查端点

常见面试问题

Q1: NestJS 的请求生命周期是怎样的?Middleware、Guard、Interceptor、Pipe、ExceptionFilter 的执行顺序?

答案

NestJS 的请求管线完整执行顺序为:

Middleware → Guard → Interceptor(前置) → Pipe → Route Handler → Interceptor(后置) → ExceptionFilter(如有异常)

详细展开:

  1. Middleware:最先执行,全局中间件 → 模块中间件,用于通用的请求预处理(日志、CORS、Cookie 解析等)
  2. Guard:全局 → 控制器 → 路由,决定请求是否有权继续(认证、授权)。返回 false 则抛出 ForbiddenException
  3. Interceptor(前置):全局 → 控制器 → 路由,可在 Handler 执行前添加逻辑(如记录开始时间)
  4. Pipe:全局 → 控制器 → 路由 → 参数级别,负责参数验证和转换
  5. Route Handler:控制器方法执行
  6. Interceptor(后置):路由 → 控制器 → 全局(反序),可对响应数据做转换(如统一包装 { code, data, message }
  7. ExceptionFilter:路由 → 控制器 → 全局,管线中任何环节抛出异常都会被捕获处理
执行顺序验证
// 全局注册各组件后,一个请求的控制台输出:
// [Middleware] → Request incoming: GET /users
// [Guard] → Checking authorization...
// [Interceptor] → Before handler...
// [Pipe] → Validating params...
// [Handler] → UserController.findAll()
// [Interceptor] → After handler, +15ms
// [Filter] → (仅在异常时触发)
关键区别
  • Middleware 无法知道下一步会执行哪个 Handler(没有 ExecutionContext
  • Guard 可以通过 ExecutionContext 获取即将执行的 Handler 和 Class 信息
  • Interceptor 可以同时处理请求和响应(通过 RxJS Observable
  • Pipe 只处理输入参数
  • ExceptionFilter 只处理异常

Q2: NestJS 的依赖注入是怎么实现的?三种注入作用域有什么区别?

答案

NestJS 的依赖注入基于 TypeScript 的装饰器元数据反射reflect-metadata)实现:

  1. @Injectable() 装饰器会在编译时将类的构造函数参数类型记录为元数据
  2. 应用启动时,IoC 容器扫描所有 Module,收集 providers
  3. 当需要实例化一个 Provider 时,容器读取其构造函数参数的类型元数据,递归解析并注入依赖
  4. 默认创建单例并缓存
DI 原理简化演示
// TypeScript 编译后,@Injectable() 会存储参数类型信息:
// Reflect.getMetadata('design:paramtypes', UserController)
// => [UserService]

// IoC 容器的简化逻辑:
function resolve<T>(target: Type<T>): T {
// 1. 读取构造函数参数类型
const params = Reflect.getMetadata('design:paramtypes', target) || [];

// 2. 递归解析每个依赖
const injections = params.map((param: Type) => resolve(param));

// 3. 实例化并返回
return new target(...injections);
}

三种作用域的区别:

作用域实例数量创建时机销毁时机适用场景
DEFAULT全局 1 个(单例)应用启动时应用关闭时无状态的 Service(绝大多数场景)
REQUEST每个请求 1 个请求到来时请求结束后 GC需要请求上下文的场景(多租户、请求级缓存)
TRANSIENT每次注入 1 个每次被注入时消费者被销毁时需要独立状态的工具类(如独立的 Logger 实例)
性能注意

REQUEST 和 TRANSIENT 作用域会频繁创建实例,影响性能。此外,REQUEST 作用域会导致作用域冒泡 -- 依赖它的上游 Provider 也会被提升为 REQUEST 作用域。建议大多数场景使用默认的 DEFAULT 作用域。

Q3: Guard 和 Middleware 有什么区别?分别适用什么场景?

答案

对比项MiddlewareGuard
执行时机最先执行,在 Guard 之前Middleware 之后,Interceptor 之前
访问上下文只有 req/res/next(Express 原生)拥有 ExecutionContext,可获取 Handler 和 Class 元数据
返回值无返回值,调用 next() 继续返回 boolean(true 放行,false 拒绝)
依赖注入类中间件支持,函数中间件不支持完全支持 DI
读取装饰器元数据不能可以(通过 Reflector
注册方式configure(consumer)app.use()@UseGuards()APP_GUARD

适用场景:

  • Middleware 适合:日志记录、CORS、请求体解析、压缩、限流(通用的 HTTP 层面的预处理,与业务逻辑无关)
  • Guard 适合:JWT 认证、角色授权、API Key 校验、IP 白名单(需要知道即将执行哪个 Handler 的访问控制场景)
典型区别:角色控制只能用 Guard
// Guard 可以读取 @Roles('admin') 装饰器设置的元数据
// Middleware 做不到,因为它不知道即将执行哪个 Handler

@Roles('admin')
@UseGuards(AuthGuard, RolesGuard)
@Get('users')
getUsers() { /* ... */ }

Q4: 如何在 NestJS 中实现统一的错误处理?

答案

NestJS 中实现统一错误处理的推荐方案是全局 ExceptionFilter + 自定义业务异常类

第一步:定义业务异常基类

src/common/exceptions/business.exception.ts
import { HttpException, HttpStatus } from '@nestjs/common';

export class BusinessException extends HttpException {
private readonly errorCode: string;

constructor(message: string, errorCode: string, status = HttpStatus.BAD_REQUEST) {
super(message, status);
this.errorCode = errorCode;
}

getErrorCode(): string {
return this.errorCode;
}
}

// 具体业务异常
export class UserExistsException extends BusinessException {
constructor(email: string) {
super(`User with email ${email} already exists`, 'USER_EXISTS', HttpStatus.CONFLICT);
}
}

第二步:全局异常过滤器统一格式

src/common/filters/http-exception.filter.ts
import {
ExceptionFilter, Catch, ArgumentsHost,
HttpException, HttpStatus, Logger,
} from '@nestjs/common';
import { Request, Response } from 'express';
import { BusinessException } from '../exceptions/business.exception';

@Catch()
export class GlobalExceptionFilter implements ExceptionFilter {
private readonly logger = new Logger('ExceptionFilter');

catch(exception: unknown, host: ArgumentsHost) {
const ctx = host.switchToHttp();
const request = ctx.getRequest<Request>();
const response = ctx.getResponse<Response>();

let status: number;
let message: string;
let errorCode: string;

if (exception instanceof BusinessException) {
// 业务异常:返回具体的业务错误码
status = exception.getStatus();
message = exception.message;
errorCode = exception.getErrorCode();
} else if (exception instanceof HttpException) {
// HTTP 异常:ValidationPipe 抛出的 400 等
status = exception.getStatus();
const res = exception.getResponse();
message = typeof res === 'string' ? res : (res as Record<string, unknown>).message as string;
errorCode = 'HTTP_ERROR';
} else {
// 未知异常:统一返回 500
status = HttpStatus.INTERNAL_SERVER_ERROR;
message = 'Internal Server Error';
errorCode = 'INTERNAL_ERROR';
this.logger.error('Unhandled exception', (exception as Error)?.stack);
}

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

第三步:注册全局 Filter

src/app.module.ts
import { APP_FILTER } from '@nestjs/core';
import { GlobalExceptionFilter } from './common/filters/http-exception.filter';

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

这样所有异常都会被统一拦截,返回一致的错误格式,前端可以根据 code 字段做差异化处理。

Q5: NestJS 相比 Express 有什么优势?什么时候该用 NestJS?

答案

对比维度ExpressNestJS
架构极简、无约束,自由组织代码强约束的模块化分层架构
TypeScript需手动配置,类型支持一般原生支持,完整类型推导
依赖注入无内置 DI内置 IoC 容器
代码组织项目大了容易混乱Module/Controller/Service 清晰分层
可测试性依赖难以 MockDI 天然支持 Mock
生态集成需自己集成各种库官方模块:ORM、GraphQL、WebSocket、微服务等
学习曲线低(几分钟上手)中等偏高(需理解 DI、装饰器、模块化)
性能更轻量略有开销(可切换 Fastify 平台提升性能)
包大小极小较大(IoC 容器 + 装饰器系统)

适合使用 NestJS 的场景:

  • 中大型团队协作的企业级项目
  • 需要清晰分层架构(Controller → Service → Repository)
  • 需要微服务架构(gRPC、消息队列等)
  • 需要 GraphQL + REST 混合 API
  • 团队有 Angular 或 Java/Spring 背景
  • 项目需要高可测试性

不需要 NestJS 的场景:

  • 简单的 API 服务(几个路由)
  • Serverless 函数(冷启动开销大)
  • 追求极致轻量和性能
  • 团队对装饰器和 DI 不熟悉且不愿学习
总结

NestJS 是 Express/Fastify 之上的架构层。选择 NestJS 不是选择一个不同的 HTTP 框架,而是选择一种架构风格。如果你的项目需要清晰的代码组织、可靠的依赖管理和完善的工程化支持,NestJS 是 Node.js 后端的首选框架。


Q6: Interceptor 和 Middleware 有什么区别?各自适合什么场景?

答案

对比项MiddlewareInterceptor
执行时机最先执行Guard 之后,Handler 前后
双向处理只能处理请求(单向)可以同时处理请求和响应(双向)
访问上下文只有 req/res拥有 ExecutionContext
操作响应需要监听 res.on('finish')通过 RxJS Observable 直接操作
DI 支持类中间件支持完全支持
Interceptor 双向处理能力
@Injectable()
export class TimingInterceptor implements NestInterceptor {
intercept(context: ExecutionContext, next: CallHandler): Observable<any> {
const start = Date.now();
return next.handle().pipe(
tap(() => {
// 在响应完成后执行(Middleware 做不到这么优雅)
console.log(`${context.getHandler().name} +${Date.now() - start}ms`);
}),
);
}
}

选择建议:通用 HTTP 预处理(CORS、body 解析、日志初始化)用 Middleware;需要操作响应数据(格式包装、缓存、超时控制)用 Interceptor。


Q7: 如何在 NestJS 中实现缓存?

答案

NestJS 提供 @nestjs/cache-manager 实现多级缓存:

模块注册
import { CacheModule } from '@nestjs/cache-manager';
import { redisStore } from 'cache-manager-redis-yet';

@Module({
imports: [
CacheModule.registerAsync({
useFactory: async () => ({
store: await redisStore({ host: 'localhost', port: 6379 }),
ttl: 60 * 1000, // 默认 60s
}),
}),
],
})
export class AppModule {}

三种使用方式

方式1:装饰器自动缓存
@Controller('products')
export class ProductController {
@UseInterceptors(CacheInterceptor) // GET 请求自动缓存
@CacheTTL(30 * 1000)
@CacheKey('all-products')
@Get()
findAll() { return this.productService.findAll(); }
}
方式2:手动操作缓存
@Injectable()
export class ProductService {
constructor(@Inject(CACHE_MANAGER) private cache: Cache) {}

async findById(id: string) {
const cached = await this.cache.get(`product:${id}`);
if (cached) return cached;

const product = await this.productRepo.findOne(id);
await this.cache.set(`product:${id}`, product, 60 * 1000);
return product;
}
}

Q8: NestJS 中如何处理循环依赖?

答案

循环依赖在模块化架构中很常见:A 依赖 B,B 也依赖 A。NestJS 提供 forwardRef 解决:

Provider 层循环依赖
// user.service.ts
@Injectable()
export class UserService {
constructor(
@Inject(forwardRef(() => OrderService))
private orderService: OrderService,
) {}
}

// order.service.ts
@Injectable()
export class OrderService {
constructor(
@Inject(forwardRef(() => UserService))
private userService: UserService,
) {}
}
Module 层循环依赖
// user.module.ts
@Module({
imports: [forwardRef(() => OrderModule)],
providers: [UserService],
exports: [UserService],
})
export class UserModule {}
循环依赖是代码异味

虽然 forwardRef 能解决问题,但循环依赖通常说明模块划分有问题。更好的做法是提取共享逻辑到第三个 SharedModule,或者使用事件驱动解耦。


Q9: 如何在 NestJS 中自定义装饰器?

答案

NestJS 支持三种自定义装饰器:

1. 参数装饰器 - 提取请求中的用户信息
import { createParamDecorator, ExecutionContext } from '@nestjs/common';

export const CurrentUser = createParamDecorator(
(data: string | undefined, ctx: ExecutionContext) => {
const request = ctx.switchToHttp().getRequest();
const user = request.user;
return data ? user?.[data] : user; // @CurrentUser('id') 只返回 id
},
);

// 使用
@Get('profile')
getProfile(@CurrentUser() user: UserEntity) {
return user;
}
2. 元数据装饰器 - 角色控制
import { SetMetadata } from '@nestjs/common';

export const ROLES_KEY = 'roles';
export const Roles = (...roles: string[]) => SetMetadata(ROLES_KEY, roles);

// Guard 中读取
const roles = this.reflector.getAllAndOverride<string[]>(ROLES_KEY, [
context.getHandler(),
context.getClass(),
]);
3. 组合装饰器 - 合并多个装饰器
import { applyDecorators, UseGuards, SetMetadata } from '@nestjs/common';

export function Auth(...roles: string[]) {
return applyDecorators(
SetMetadata('roles', roles),
UseGuards(AuthGuard, RolesGuard),
);
}

// 使用:一个装饰器搞定认证 + 角色
@Auth('admin')
@Delete(':id')
remove(@Param('id') id: string) { /* ... */ }

Q10: NestJS 中 ValidationPipe 怎么用?如何做参数校验?

答案

NestJS 推荐使用 class-validator + class-transformer + ValidationPipe 做声明式参数校验:

DTO 定义
import { IsString, IsEmail, MinLength, IsOptional, IsEnum } from 'class-validator';
import { Transform } from 'class-transformer';

export class CreateUserDto {
@IsString()
@MinLength(2, { message: '用户名至少 2 个字符' })
name: string;

@IsEmail({}, { message: '邮箱格式不正确' })
@Transform(({ value }) => value?.toLowerCase().trim()) // 自动转换
email: string;

@IsString()
@MinLength(8)
password: string;

@IsOptional()
@IsEnum(['admin', 'user'])
role?: string;
}
全局注册 ValidationPipe
// main.ts
app.useGlobalPipes(
new ValidationPipe({
whitelist: true, // 自动剥离 DTO 中未定义的字段
forbidNonWhitelisted: true, // 未定义字段直接报错
transform: true, // 自动类型转换(string → number 等)
}),
);

校验失败时自动返回 400 错误,包含详细的字段级错误信息,无需手动 if-else 校验。


Q11: NestJS 的定时任务怎么实现?多实例部署怎么防重复执行?

答案

定时任务基础
import { Injectable } from '@nestjs/common';
import { Cron, CronExpression, Interval } from '@nestjs/schedule';

@Injectable()
export class TaskService {
@Cron(CronExpression.EVERY_HOUR) // 每小时
async handleHourlyTask() {
await this.cleanExpiredTokens();
}

@Interval(30000) // 每 30 秒
async handleHealthCheck() {
await this.checkExternalServices();
}
}

多实例防重复需要分布式锁:

Redis 分布式锁封装
@Injectable()
export class DistributedCron {
constructor(private redis: Redis) {}

async runWithLock(lockKey: string, ttl: number, task: () => Promise<void>) {
const acquired = await this.redis.set(lockKey, process.env.POD_NAME, 'NX', 'EX', ttl);
if (!acquired) return; // 其他实例已获取锁

try {
await task();
} finally {
await this.redis.del(lockKey); // 任务完成主动释放
}
}
}

Q12: NestJS 如何实现微服务架构?

答案

NestJS 内置多种微服务传输层:

传输方式特点适用场景
TCP内置,零依赖开发/测试
RedisPub/Sub 模式轻量级事件驱动
NATS高性能消息系统高吞吐微服务
Kafka持久化消息流事件溯源、日志
gRPC强类型 RPC跨语言、高性能
MQTT轻量级协议IoT 设备通信
微服务端
// order-service/main.ts
const app = await NestFactory.createMicroservice(OrderModule, {
transport: Transport.TCP,
options: { host: '0.0.0.0', port: 3001 },
});
await app.listen();
网关端调用
// api-gateway/order.controller.ts
@Controller('orders')
export class OrderController {
constructor(
@Inject('ORDER_SERVICE') private orderClient: ClientProxy,
) {}

@Post()
createOrder(@Body() dto: CreateOrderDto) {
return this.orderClient.send('create_order', dto); // 发送消息
}
}

Q13: 如何在 NestJS 中实现全局的请求日志和链路追踪?

答案

通过 Middleware + Interceptor 组合实现:

TraceMiddleware - 生成 traceId
@Injectable()
export class TraceMiddleware implements NestMiddleware {
use(req: Request, res: Response, next: NextFunction) {
const traceId = req.headers['x-trace-id'] as string || randomUUID();
req['traceId'] = traceId;
res.setHeader('x-trace-id', traceId);
next();
}
}
LoggingInterceptor - 请求耗时记录
@Injectable()
export class LoggingInterceptor implements NestInterceptor {
private readonly logger = new Logger('HTTP');

intercept(context: ExecutionContext, next: CallHandler): Observable<any> {
const req = context.switchToHttp().getRequest();
const { method, url, traceId } = req;
const start = Date.now();

return next.handle().pipe(
tap(() => {
const ms = Date.now() - start;
this.logger.log(`[${traceId}] ${method} ${url} ${ms}ms`);
}),
catchError((err) => {
const ms = Date.now() - start;
this.logger.error(`[${traceId}] ${method} ${url} ${ms}ms - ${err.message}`);
throw err;
}),
);
}
}

通过 traceId 可以串联一个请求在所有服务中的完整调用链。


Q14: NestJS 怎么做单元测试?如何 Mock 依赖?

答案

NestJS 的 DI 机制天然支持 Mock,使用 @nestjs/testing 模块:

user.service.spec.ts
import { Test, TestingModule } from '@nestjs/testing';
import { UserService } from './user.service';
import { getRepositoryToken } from '@nestjs/typeorm';
import { User } from './user.entity';

describe('UserService', () => {
let service: UserService;
const mockRepo = {
findOne: jest.fn(),
save: jest.fn(),
create: jest.fn(),
};

beforeEach(async () => {
const module: TestingModule = await Test.createTestingModule({
providers: [
UserService,
{ provide: getRepositoryToken(User), useValue: mockRepo }, // Mock Repository
],
}).compile();

service = module.get<UserService>(UserService);
});

it('should find user by id', async () => {
const user = { id: '1', name: 'Alice', email: 'a@test.com' };
mockRepo.findOne.mockResolvedValue(user);

const result = await service.findById('1');

expect(result).toEqual(user);
expect(mockRepo.findOne).toHaveBeenCalledWith({ where: { id: '1' } });
});

it('should throw if user not found', async () => {
mockRepo.findOne.mockResolvedValue(null);
await expect(service.findById('999')).rejects.toThrow('User not found');
});
});
Mock 策略
  • Repository:Mock 数据库方法(find, save, delete
  • HttpService:Mock axios 请求
  • ConfigService:使用 useValue 提供测试配置
  • Redis/Cache:Mock get/set 方法

Q15: NestJS 如何切换底层 HTTP 引擎为 Fastify?

答案

NestJS 的平台抽象层允许零成本切换底层引擎:

main.ts
import { NestFactory } from '@nestjs/core';
import { FastifyAdapter, NestFastifyApplication } from '@nestjs/platform-fastify';
import { AppModule } from './app.module';

async function bootstrap() {
const app = await NestFactory.create<NestFastifyApplication>(
AppModule,
new FastifyAdapter({ logger: true }),
);
await app.listen(3000, '0.0.0.0'); // Fastify 需要显式绑定 0.0.0.0
}
bootstrap();

切换注意事项

维度Express 适配器Fastify 适配器
性能基准提升约 2x
Express 中间件直接使用需要 @fastify/middie 适配
req/res 对象Express 类型Fastify 类型(API 不同)
文件上传multer@fastify/multipart
静态文件express.static@fastify/static
注意

如果项目大量使用了 Express 特有的中间件(如 passportmulter),切换 Fastify 需要替换为对应的 Fastify 插件。建议新项目从一开始就选定引擎。


相关链接