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 |
排查顺序:
- 看启动日志:依赖注入错误会明确给出类名与索引。
- 检查模块的
providers/exports/imports。 - 检查 tsconfig 的装饰器选项。
- 用
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()