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/Koa | NestJS |
|---|---|---|
| 定位 | HTTP 库/框架 | 企业级应用框架 |
| 架构 | 自由组织 | 强约束模块化 |
| 依赖注入 | 无内置 | 内置 IoC 容器 |
| TypeScript | 手动配置 | 原生支持 |
| 学习曲线 | 低 | 中等偏高 |
| 适用场景 | 小型项目、微服务 | 中大型项目、企业应用 |
- 企业级架构:内置分层结构,团队协作更规范
- 丰富的生态:官方提供 TypeORM、Mongoose、GraphQL、WebSocket、微服务等模块
- 高可测试性:依赖注入天然支持 Mock 和单元测试
- 适合构建 BFF 网关层、微服务、REST API、GraphQL 服务等
快速上手
- npm
- Yarn
- pnpm
- Bun
npm install @nestjs/cli -g
yarn global add @nestjs/cli
pnpm add @nestjs/cli -g
bun add @nestjs/cli --global
nest new my-project
生成的项目结构:
src/
├── app.module.ts # 根模块
├── app.controller.ts # 根控制器
├── app.service.ts # 根服务
└── 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 的三大核心构件是 Module、Controller 和 Provider。
Module(模块)
模块是 NestJS 组织代码的基本单元,使用 @Module() 装饰器声明。
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 请求,使用路由装饰器映射请求路径。
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(服务层,负责业务逻辑)。
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 容器会:
- 扫描所有 Module 中注册的 Provider
- 解析每个 Provider 构造函数的参数类型(通过 TypeScript 的
emitDecoratorMetadata反射元数据) - 构建依赖图并按正确顺序实例化
- 缓存实例(默认单例)
注入作用域
NestJS 提供三种注入作用域:
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 方式:
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:
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 打破循环:
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,
) {}
}
循环依赖说明模块设计存在耦合问题。推荐的解决方式:
- 提取公共逻辑到第三个 Service
- 使用事件驱动(EventEmitter)解耦
- 重新审视模块边界
4. 请求生命周期
NestJS 拥有一套完整的请求管线,每个请求按固定顺序流经各环节。理解这条管线是掌握 NestJS 的关键。
各环节详细执行顺序:
M-G-I-P-H-I-E:Middleware → Guard → Interceptor → Pipe → Handler → Interceptor → ExceptionFilter
| 环节 | 职责 | 典型用途 |
|---|---|---|
| Middleware | 请求预处理 | 日志、CORS、Cookie 解析 |
| Guard | 访问控制(返回 boolean) | 认证、授权、RBAC |
| Interceptor | 请求/响应转换 | 日志、缓存、响应包装、超时 |
| Pipe | 数据验证与转换 | DTO 验证、参数类型转换 |
| Handler | 业务处理 | 控制器方法 |
| ExceptionFilter | 异常处理 | 统一错误格式、异常日志 |
5. 中间件(Middleware)
NestJS 中间件本质上就是 Express 中间件,在路由处理之前执行。
类中间件 vs 函数中间件
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();
}
注册中间件
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 中注册):
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。
基本结构
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)
结合自定义装饰器实现角色控制:
import { SetMetadata } from '@nestjs/common';
export const ROLES_KEY = 'roles';
export const Roles = (...roles: string[]) => SetMetadata(ROLES_KEY, roles);
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 实现,功能强大且灵活。
响应包装拦截器
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,
})),
);
}
}
日志拦截器
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`); // 后置:请求完成
}),
);
}
}
超时拦截器
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);
}),
);
}
}
缓存拦截器
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 提供多个开箱即用的管道:
| 管道 | 用途 |
|---|---|
ValidationPipe | DTO 验证(配合 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
- Yarn
- pnpm
- Bun
npm install class-validator class-transformer
yarn add class-validator class-transformer
pnpm add class-validator class-transformer
bun add class-validator class-transformer
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;
}
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: true 后,客户端传入 DTO 中未声明的字段会被自动过滤,有效防止批量赋值攻击(Mass Assignment)。例如,用户注册时偷偷传入 role: 'admin' 会被自动剔除。
自定义管道
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 状态码 |
|---|---|
BadRequestException | 400 |
UnauthorizedException | 401 |
ForbiddenException | 403 |
NotFoundException | 404 |
MethodNotAllowedException | 405 |
ConflictException | 409 |
UnprocessableEntityException | 422 |
InternalServerErrorException | 500 |
ServiceUnavailableException | 503 |
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;
}
}
自定义全局异常过滤器
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);
}
}
注册全局异常过滤器:
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);
}
如果异常过滤器需要注入其他 Provider(如 Logger Service),应使用 Module 方式注册:
import { APP_FILTER } from '@nestjs/core';
@Module({
providers: [
{
provide: APP_FILTER,
useClass: AllExceptionFilter,
},
],
})
export class AppModule {}
同理,APP_GUARD、APP_INTERCEPTOR、APP_PIPE 也支持这种注册方式。
10. 其他重要特性
定时任务(@nestjs/schedule)
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)
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',
},
});
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 {}
@Injectable()
export class DatabaseService {
constructor(private configService: ConfigService) {}
getDbHost(): string {
return this.configService.get<string>('database.host', 'localhost');
}
}
限流(@nestjs/throttler)
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 网关
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/schedule | Cron/Interval/Timeout 定时任务 |
| 配置管理 | @nestjs/config | 环境变量、配置文件管理 |
| 缓存 | @nestjs/cache-manager | 内存/Redis 缓存 |
| 限流 | @nestjs/throttler | 请求频率限制 |
| WebSocket | @nestjs/websockets | 实时通信 |
| GraphQL | @nestjs/graphql | GraphQL API |
| 微服务 | @nestjs/microservices | TCP/Redis/NATS/gRPC 微服务 |
| TypeORM | @nestjs/typeorm | 数据库 ORM 集成 |
| Mongoose | @nestjs/mongoose | MongoDB ODM 集成 |
| Swagger | @nestjs/swagger | API 文档自动生成 |
| 健康检查 | @nestjs/terminus | 应用健康检查端点 |
常见面试问题
Q1: NestJS 的请求生命周期是怎样的?Middleware、Guard、Interceptor、Pipe、ExceptionFilter 的执行顺序?
答案:
NestJS 的请求管线完整执行顺序为:
Middleware → Guard → Interceptor(前置) → Pipe → Route Handler → Interceptor(后置) → ExceptionFilter(如有异常)
详细展开:
- Middleware:最先执行,全局中间件 → 模块中间件,用于通用的请求预处理(日志、CORS、Cookie 解析等)
- Guard:全局 → 控制器 → 路由,决定请求是否有权继续(认证、授权)。返回
false则抛出ForbiddenException - Interceptor(前置):全局 → 控制器 → 路由,可在 Handler 执行前添加逻辑(如记录开始时间)
- Pipe:全局 → 控制器 → 路由 → 参数级别,负责参数验证和转换
- Route Handler:控制器方法执行
- Interceptor(后置):路由 → 控制器 → 全局(反序),可对响应数据做转换(如统一包装
{ code, data, message }) - 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)实现:
@Injectable()装饰器会在编译时将类的构造函数参数类型记录为元数据- 应用启动时,IoC 容器扫描所有 Module,收集 providers
- 当需要实例化一个 Provider 时,容器读取其构造函数参数的类型元数据,递归解析并注入依赖
- 默认创建单例并缓存
// 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 有什么区别?分别适用什么场景?
答案:
| 对比项 | Middleware | Guard |
|---|---|---|
| 执行时机 | 最先执行,在 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 可以读取 @Roles('admin') 装饰器设置的元数据
// Middleware 做不到,因为它不知道即将执行哪个 Handler
@Roles('admin')
@UseGuards(AuthGuard, RolesGuard)
@Get('users')
getUsers() { /* ... */ }
Q4: 如何在 NestJS 中实现统一的错误处理?
答案:
NestJS 中实现统一错误处理的推荐方案是全局 ExceptionFilter + 自定义业务异常类:
第一步:定义业务异常基类
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);
}
}
第二步:全局异常过滤器统一格式
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
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?
答案:
| 对比维度 | Express | NestJS |
|---|---|---|
| 架构 | 极简、无约束,自由组织代码 | 强约束的模块化分层架构 |
| TypeScript | 需手动配置,类型支持一般 | 原生支持,完整类型推导 |
| 依赖注入 | 无内置 DI | 内置 IoC 容器 |
| 代码组织 | 项目大了容易混乱 | Module/Controller/Service 清晰分层 |
| 可测试性 | 依赖难以 Mock | DI 天然支持 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 有什么区别?各自适合什么场景?
答案:
| 对比项 | Middleware | Interceptor |
|---|---|---|
| 执行时机 | 最先执行 | Guard 之后,Handler 前后 |
| 双向处理 | 只能处理请求(单向) | 可以同时处理请求和响应(双向) |
| 访问上下文 | 只有 req/res | 拥有 ExecutionContext |
| 操作响应 | 需要监听 res.on('finish') | 通过 RxJS Observable 直接操作 |
| DI 支持 | 类中间件支持 | 完全支持 |
@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 {}
三种使用方式:
@Controller('products')
export class ProductController {
@UseInterceptors(CacheInterceptor) // GET 请求自动缓存
@CacheTTL(30 * 1000)
@CacheKey('all-products')
@Get()
findAll() { return this.productService.findAll(); }
}
@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 解决:
// 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,
) {}
}
// user.module.ts
@Module({
imports: [forwardRef(() => OrderModule)],
providers: [UserService],
exports: [UserService],
})
export class UserModule {}
虽然 forwardRef 能解决问题,但循环依赖通常说明模块划分有问题。更好的做法是提取共享逻辑到第三个 SharedModule,或者使用事件驱动解耦。
Q9: 如何在 NestJS 中自定义装饰器?
答案:
NestJS 支持三种自定义装饰器:
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;
}
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(),
]);
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 做声明式参数校验:
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;
}
// 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();
}
}
多实例防重复需要分布式锁:
@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 | 内置,零依赖 | 开发/测试 |
| Redis | Pub/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 组合实现:
@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();
}
}
@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 模块:
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');
});
});
- Repository:Mock 数据库方法(
find,save,delete) - HttpService:Mock
axios请求 - ConfigService:使用
useValue提供测试配置 - Redis/Cache:Mock
get/set方法
Q15: NestJS 如何切换底层 HTTP 引擎为 Fastify?
答案:
NestJS 的平台抽象层允许零成本切换底层引擎:
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 特有的中间件(如 passport、multer),切换 Fastify 需要替换为对应的 Fastify 插件。建议新项目从一开始就选定引擎。