POST · 后端 · 文章

NestJS 从入门到进阶

2026/11/13 · by ZHANG XING YUN · 阅读 1 次 · 3371 字 · 预计阅读 9 分钟

本文覆盖 NestJS 的架构模型(模块、控制器、提供者、依赖注入)、请求生命周期、参数校验、配置管理、数据库集成、测试与常见错误,可作为学习与查漏的参考。

  • 环境:Node.js 20+、NestJS 11、TypeScript 5
  • 前置知识:TypeScript 基础(装饰器、泛型、接口)、HTTP 与 REST

一、NestJS 解决什么问题

Node 生态的 Web 框架很多(Express、Koa、Fastify),它们只提供路由与中间件,项目变大后常见问题:

  • 目录结构、分层方式各写一套,新人上手成本高。
  • 依赖管理靠手工传递,测试时难以替换。
  • 参数校验、异常处理、日志、配置散落各处。

NestJS 在底层 HTTP 框架(默认 Express,可换 Fastify)之上提供了:

能力 实现方式
模块化 @Module 声明边界与依赖
依赖注入 构造器注入 + 装饰器元数据
请求管线 中间件 → 守卫 → 拦截器 → 管道 → 控制器
参数校验 ValidationPipe + class-validator
统一异常 HttpException + ExceptionFilter
可替换底层 NestFactory.create(..., { adapter })

代价是需要理解装饰器与依赖注入,且启动元数据依赖 emitDecoratorMetadata。


二、安装与创建项目

npm i -g @nestjs/cli
nest new my-app          # 选择包管理器
cd my-app
npm run start:dev        # 开发模式(watch)
npm run build            # 编译到 dist
npm run start:prod       # node dist/main

不使用 CLI 也可以,最小依赖:

npm i @nestjs/common @nestjs/core @nestjs/platform-express reflect-metadata rxjs
npm i -D typescript ts-node @types/node

tsconfig.json 必选项:

{
	"compilerOptions": {
		"experimentalDecorators": true,
		"emitDecoratorMetadata": true,
		"target": "ES2022",
		"module": "commonjs",
		"strict": true,
		"strictPropertyInitialization": false
	}
}

缺少 emitDecoratorMetadata 时,依赖注入无法按类型解析,启动会报"无法解析依赖"。


三、核心概念:Module / Controller / Provider

应用(AppModule)
├─ UsersModule
│  ├─ UsersController   ← 接收 HTTP 请求
│  └─ UsersService      ← 业务逻辑(Provider)
└─ DatabaseModule
   └─ DataSource        ← 被注入的 Provider
// src/app.module.ts
import { Module } from '@nestjs/common';
import { UsersModule } from './users/users.module';

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

@Module 的四个字段:

字段 作用
imports 引入其它模块(被引入模块导出的 Provider 才可用)
controllers 本模块的控制器
providers 本模块的提供者(服务、工厂、值)
exports 对外暴露的提供者

模块是边界:Provider 默认只在所属模块内可见,跨模块使用必须 exports + imports。


四、控制器与路由

// src/users/users.controller.ts
import { Body, Controller, Get, Param, Post, Query } from '@nestjs/common';
import { UsersService } from './users.service';
import { CreateUserDto } from './dto/create-user.dto';

@Controller('users')          // 路由前缀 /users
export class UsersController {
	constructor(private readonly users: UsersService) {}

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

	@Get()
	findAll(@Query('page') page = '1') {
		return this.users.findAll(Number(page));
	}

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

4.1 参数装饰器

装饰器 取什么
@Param('id') 路径参数
@Query('page') 查询字符串
@Body() 请求体(配合 DTO)
@Headers('authorization') 请求头
@Req() / @Res() 原生请求/响应对象
@Ip() 客户端 IP
@Session() 会话(需中间件)

使用 @Res() 会接管响应,此时需要手动 res.json(),否则请求会挂起。多数场景用返回值即可。

4.2 状态码与响应头

import { HttpCode, Header, Post } from '@nestjs/common';

@Post()
@HttpCode(201)
@Header('Cache-Control', 'no-store')
create(@Body() dto: CreateUserDto) {
	return this.users.create(dto);
}

4.3 路由分组与版本

@Controller({ path: 'users', version: '1' })

配合 app.enableVersioning({ type: VersioningType.URI }),路由变为 /v1/users。


五、提供者与依赖注入

// src/users/users.service.ts
import { Injectable, NotFoundException } from '@nestjs/common';

@Injectable()
export class UsersService {
	private readonly users: { id: string; name: string }[] = [];

	create(input: { name: string }) {
		const user = { id: String(Date.now()), name: input.name };
		this.users.push(user);
		return user;
	}

	findAll(page: number) {
		return this.users.slice((page - 1) * 20, page * 20);
	}

	findOne(id: string) {
		const user = this.users.find(item => item.id === id);
		if (!user) throw new NotFoundException(`用户 ${id} 不存在`);
		return user;
	}
}
// src/users/users.module.ts
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 {}

5.1 自定义 Provider

// 1) 值
{ provide: 'API_KEY', useValue: process.env.API_KEY }

// 2) 类
{ provide: CacheService, useClass: RedisCacheService }

// 3) 工厂(可异步,适合连接数据库)
{
	provide: 'DB',
	inject: [ConfigService],
	useFactory: async (config: ConfigService) => createConnection(config.get('DB_URL'))
}

使用:

constructor(
	@Inject('API_KEY') private readonly apiKey: string,
	@Inject('DB') private readonly db: Db
) {}

5.2 作用域

作用域 说明
DEFAULT(单例) 整个应用共享一个实例
REQUEST 每个请求一个实例(性能开销大)
TRANSIENT 每次注入创建新实例
@Injectable({ scope: Scope.REQUEST })
export class RequestContextService {}

优先使用单例。请求作用域会沿依赖链扩散,导致大量实例创建。

5.3 循环依赖

A 依赖 B、B 依赖 A 时,Nest 会报 Nest can't resolve dependencies。解决方式:

@Injectable()
export class AService {
	constructor(@Inject(forwardRef(() => BService)) private readonly b: BService) {}
}

更推荐的做法是重构:把公共逻辑抽到第三个服务,打断循环。


六、请求生命周期与执行顺序

请求
 → 中间件 Middleware
 → 守卫 Guard            (是否放行:鉴权、角色)
 → 拦截器 Interceptor(前) (日志、计时、转换入参)
 → 管道 Pipe             (校验、类型转换)
 → 控制器方法
 → 服务 Provider
 → 拦截器 Interceptor(后)
 → 异常过滤器 ExceptionFilter(有异常时)
 → 响应
环节 关注点 典型用途
中间件 请求进入、无 Nest 上下文 日志、请求 ID、CORS
守卫 是否允许访问 JWT 鉴权、角色校验
拦截器 前后包裹 统一响应、耗时统计、缓存
管道 参数转换与校验 DTO 校验、ParseIntPipe
过滤器 异常兜底 统一错误格式

顺序记住一点:守卫在管道之前,因此守卫里拿不到校验后的 DTO;需要在守卫中读 body 时,自行解析。


七、中间件、守卫、拦截器、管道、过滤器

7.1 中间件

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): void {
		const start = Date.now();
		res.on('finish', () => {
			console.log(`${req.method} ${req.originalUrl} ${res.statusCode} ${Date.now() - start}ms`);
		});
		next();
	}
}
// 模块中注册
export class AppModule implements NestModule {
	configure(consumer: MiddlewareConsumer): void {
		consumer.apply(LoggerMiddleware).forRoutes('*');
	}
}

7.2 守卫

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

@Injectable()
export class AuthGuard implements CanActivate {
	canActivate(context: ExecutionContext): boolean {
		const request = context.switchToHttp().getRequest<{ headers: Record<string, string> }>();
		const token = request.headers.authorization?.replace('Bearer ', '');
		if (!token) throw new UnauthorizedException('缺少 token');
		return true;
	}
}
@UseGuards(AuthGuard)
@Controller('orders')
export class OrdersController {}

7.3 拦截器

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

@Injectable()
export class ResponseInterceptor implements NestInterceptor {
	intercept(context: ExecutionContext, next: CallHandler): Observable<unknown> {
		const start = Date.now();
		return next.handle().pipe(
			tap(() => console.log(`耗时 ${Date.now() - start}ms`)),
			map(data => ({ code: 0, message: 'ok', data }))
		);
	}
}

拦截器基于 RxJS,next.handle() 返回 Observable,用 map/tap/catchError 改写结果或错误。

7.4 管道

内置管道:

@Get(':id')
findOne(@Param('id', ParseIntPipe) id: number) {}

自定义管道:

import { ArgumentMetadata, Injectable, PipeTransform, BadRequestException } from '@nestjs/common';

@Injectable()
export class TrimPipe implements PipeTransform<string, string> {
	transform(value: string, metadata: ArgumentMetadata): string {
		if (metadata.type !== 'body' && metadata.type !== 'query') return value;
		return typeof value === 'string' ? value.trim() : value;
	}
}

全局管道(推荐):

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

7.5 异常过滤器

import { ArgumentsHost, Catch, ExceptionFilter, HttpException, HttpStatus } from '@nestjs/common';
import { Request, Response } from 'express';

@Catch()
export class AllExceptionsFilter implements ExceptionFilter {
	catch(exception: unknown, host: ArgumentsHost): void {
		const ctx = host.switchToHttp();
		const response = ctx.getResponse<Response>();
		const request = ctx.getRequest<Request>();

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

		const message = exception instanceof HttpException
			? exception.getResponse()
			: '服务器内部错误';

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

注册:app.useGlobalFilters(new AllExceptionsFilter()),或用 APP_FILTER 提供者(可注入依赖)。


八、参数校验与 DTO

npm i class-validator class-transformer
// src/users/dto/create-user.dto.ts
import { IsEmail, IsInt, IsOptional, Length, Min } from 'class-validator';

export class CreateUserDto {
	@Length(2, 64)
	name: string;

	@IsEmail()
	email: string;

	@IsOptional()
	@IsInt()
	@Min(0)
	points?: number;
}

全局启用:

app.useGlobalPipes(
	new ValidationPipe({
		whitelist: true,               // 剥离未声明字段
		forbidNonWhitelisted: true,    // 出现未知字段直接 400
		transform: true,               // 按类型转换
		transformOptions: { enableImplicitConversion: true }
	})
);

常用装饰器:

装饰器 作用
@IsString / @IsInt / @IsBoolean 类型
@IsEmail / @IsUrl / @IsUUID 格式
@Length(min, max) / @MaxLength 字符串长度
@Min / @Max 数值范围
@IsOptional 允许缺省
@IsArray + @IsString({ each: true }) 数组与元素类型
@ValidateNested + @Type(() => Child) 嵌套对象校验
@IsEnum 枚举

whitelist 与 forbidNonWhitelisted 的区别:前者静默剥离,后者直接报错。对外 API 建议用后者,能及早发现前端多传字段。


九、配置管理

npm i @nestjs/config
// src/app.module.ts
import { ConfigModule } from '@nestjs/config';

@Module({
	imports: [
		ConfigModule.forRoot({
			isGlobal: true,
			envFilePath: ['.env.local', '.env'],
			validate: raw => {
				if (!raw.DATABASE_URL) {
					throw new Error('缺少环境变量 DATABASE_URL');
				}
				return raw;
			}
		})
	]
})
export class AppModule {}
// 使用
import { ConfigService } from '@nestjs/config';

@Injectable()
export class DatabaseService {
	constructor(private readonly config: ConfigService) {}

	get url(): string {
		return this.config.getOrThrow<string>('DATABASE_URL');
	}
}

要点:

  • isGlobal: true 后其它模块无需重复 import。
  • 启动时校验缺失变量,避免运行到第一个请求才报错。
  • 不要把配置直接 process.env 到处读,集中到 ConfigService 便于测试与替换。

十、数据库集成概览

10.1 TypeORM(关系型)

// app.module.ts
TypeOrmModule.forRootAsync({
	inject: [ConfigService],
	useFactory: (config: ConfigService) => ({
		type: 'postgres',
		url: config.getOrThrow('DATABASE_URL'),
		entities: [User],
		synchronize: config.get('NODE_ENV') !== 'production',
		logging: ['error', 'warn']
	})
})
// entity
import { Column, Entity, PrimaryGeneratedColumn } from 'typeorm';

@Entity('users')
export class User {
	@PrimaryGeneratedColumn('uuid')
	id: string;

	@Column({ length: 64 })
	name: string;
}
// 使用
@Injectable()
export class UsersService {
	constructor(
		@InjectRepository(User) private readonly users: Repository<User>
	) {}

	findAll() {
		return this.users.find({ take: 20 });
	}
}

10.2 Mongoose(文档型)

MongooseModule.forRootAsync({
	inject: [ConfigService],
	useFactory: (config: ConfigService) => ({ uri: config.getOrThrow('MONGO_URL') })
})
@Schema({ timestamps: true })
export class Note {
	@Prop({ required: true })
	title: string;
}

export const NoteSchema = SchemaFactory.createForClass(Note);

@Module({
	imports: [MongooseModule.forFeature([{ name: Note.name, schema: NoteSchema }])]
})
export class NotesModule {}

10.3 选择建议

需求 选择
事务、关联、强约束 TypeORM / Prisma + PostgreSQL
结构灵活、文档型 Mongoose + MongoDB
需要类型安全与迁移 Prisma 或 TypeORM 迁移

synchronize: true 仅限开发环境,生产必须用迁移。


十一、异常处理与统一响应

import { BadRequestException, NotFoundException } from '@nestjs/common';

throw new BadRequestException('参数不合法');
throw new NotFoundException(`用户 ${id} 不存在`);

内置异常与状态码:

异常 状态码
BadRequestException 400
UnauthorizedException 401
ForbiddenException 403
NotFoundException 404
ConflictException 409
UnprocessableEntityException 422
InternalServerErrorException 500
ServiceUnavailableException 503

两种响应风格:

  • 直接返回数据:REST 风格,状态码表达结果。
  • 统一包装 { code, message, data }:用拦截器统一包装,适合与既有前端约定兼容。

不要混用"HTTP 200 + body 里 code 500",会让监控、网关、重试策略失真。


十二、日志与生命周期

import { Logger } from '@nestjs/common';

const logger = new Logger('UsersService');
logger.log('创建用户');
logger.warn('缓存未命中');
logger.error('写入失败', error instanceof Error ? error.stack : undefined);

生命周期钩子:

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

@Injectable()
export class AppService implements OnModuleInit, OnModuleDestroy {
	async onModuleInit(): Promise<void> {
		// 建立连接、预热缓存
	}

	async onModuleDestroy(): Promise<void> {
		// 关闭连接、释放资源
	}
}

要让 SIGTERM 触发销毁钩子,需要:

app.enableShutdownHooks();

否则容器编排停止容器时,连接不会优雅关闭。


十三、测试

npm i -D jest @types/jest ts-jest @nestjs/testing
// users.service.spec.ts
import { Test } from '@nestjs/testing';
import { UsersService } from './users.service';

describe('UsersService', () => {
	let service: UsersService;

	beforeEach(async () => {
		const moduleRef = await Test.createTestingModule({
			providers: [UsersService]
		}).compile();
		service = moduleRef.get(UsersService);
	});

	it('创建后可以查到', () => {
		const user = service.create({ name: '张三' });
		expect(service.findOne(user.id).name).toBe('张三');
	});
});

E2E 测试:

import { INestApplication } from '@nestjs/common';
import { Test } from '@nestjs/testing';
import request from 'supertest';
import { AppModule } from '../src/app.module';

describe('users (e2e)', () => {
	let app: INestApplication;

	beforeAll(async () => {
		const moduleRef = await Test.createTestingModule({ imports: [AppModule] }).compile();
		app = moduleRef.createNestApplication();
		await app.init();
	});

	it('GET /users', async () => {
		await request(app.getHttpServer()).get('/users').expect(200);
	});

	afterAll(async () => {
		await app.close();
	});
});

单元测试用 Test.createTestingModule 覆盖依赖,把数据库、外部服务替换为 mock,避免测试依赖环境。


十四、常见错误

错误 原因 处理
Nest can't resolve dependencies of the X (?) Provider 未注册、未 exports、循环依赖、令牌为 undefined 检查模块 imports/providers;循环依赖用 forwardRef 或重构
Cannot find module '@nestjs/core' 依赖未安装或版本不匹配 重新安装,检查 Nest 各包版本一致
装饰器报 experimentalDecorators 未启用 tsconfig 缺项 开启 experimentalDecorators 与 emitDecoratorMetadata
参数校验不生效 未注册全局 ValidationPipe,或 DTO 未用装饰器 注册管道,DTO 加装饰器
请求一直挂起 使用了 @Res() 但未返回响应 用 @Res({ passthrough: true }) 或手动 res.json()
405 Method Not Allowed 路由路径或方法未声明 检查 @Get/@Post 与 @Controller 前缀
生产环境 synchronize 改表 TypeORM 配置未按环境区分 生产禁用,改用迁移
容器停止后连接未释放 未调用 enableShutdownHooks 补上,并实现 onModuleDestroy

排查顺序:

  1. 看启动日志:依赖注入错误会明确给出类名与索引。
  2. 检查模块的 providers / exports / imports。
  3. 检查 tsconfig 的装饰器选项。
  4. 用 nest start --debug 或断点定位请求管线。

十五、FAQ

Q1:NestJS 和 Express 怎么选?

小脚本、单文件服务用 Express 更直接;需要分层、依赖注入、统一校验与测试的中大型项目用 NestJS。

Q2:Module 一定要拆分吗?

按业务域拆,一个域一个模块。拆太细会导致 imports 复杂,拆太粗会失去边界意义。

Q3:Provider 和 Service 是一回事吗?

Service 是 Provider 的一种(带 @Injectable 的类)。Provider 还包括值、工厂、类替换等。

Q4:守卫里能拿到校验后的 DTO 吗?

不能。守卫在管道之前执行。需要在守卫中校验参数时,自行解析或用自定义装饰器。

Q5:拦截器能修改响应吗?

可以。map 改数据,catchError 改错误;但不要用它做重业务逻辑。

Q6:@Res() 和返回值能混用吗?

默认不能。用 @Res({ passthrough: true }) 可以在拿到响应对象的同时保留 Nest 的返回值处理。

Q7:如何做全局前缀与版本?

app.setGlobalPrefix('api');
app.enableVersioning({ type: VersioningType.URI });

Q8:如何在启动时校验环境变量?

ConfigModule.forRoot({ validate }),缺少必要变量时直接抛错,让进程启动失败。

Q9:请求作用域什么时候用?

需要在一次请求内共享上下文(如当前用户、traceId)且不想显式传参时。它会影响依赖链性能,能不用就不用。

Q10:单元测试怎么替换数据库?

用 Test.createTestingModule 覆盖 provider:

.overrideProvider(UsersRepository).useValue(mockRepository)

附录:装饰器速查

// 模块
@Module({ imports, controllers, providers, exports })
@Global()

// 控制器与路由
@Controller('path')
@Get() @Post() @Put() @Patch() @Delete()
@Param() @Query() @Body() @Headers() @Req() @Res() @Ip()
@HttpCode(201) @Header('k','v')
@UseGuards() @UseInterceptors() @UsePipes() @UseFilters()

// 提供者
@Injectable()
@Inject('TOKEN')
@Optional()
@Global()

// 生命周期
OnModuleInit onModuleInit()
OnModuleDestroy onModuleDestroy()
OnApplicationBootstrap onApplicationBootstrap()
BeforeApplicationShutdown beforeApplicationShutdown()
OnApplicationShutdown onApplicationShutdown()

// 管线组件
CanActivate canActivate()
NestInterceptor intercept()
PipeTransform transform()
ExceptionFilter catch()
NestMiddleware use()
← 返回帖子 更多后端内容 →