NestJS 知识总结(基础 + 进阶 + 实战)
NestJS 是基于 TypeScript 的 Node.js 服务端框架,融合 OOP、FP 与响应式编程,受 Angular 启发。本文覆盖模块、控制器、依赖注入、数据库、认证与部署。
第一部分:基础
一、NestJS 简介
1.1 什么是 NestJS
- 基于 TypeScript 的 Node.js 后端框架
- 核心依赖 Express(默认)或 Fastify 作为 HTTP 引擎
- 采用模块化架构,内置依赖注入(DI)
- 高度借鉴 Angular 的设计理念
- 官方文档:https://docs.nestjs.com/
1.2 NestJS vs Express vs Koa
| 对比项 | NestJS | Express | Koa |
|---|---|---|---|
| 语言 | TypeScript 优先 | JavaScript | JavaScript |
| 架构 | 约定优于配置,模块化 | 自由组合,无约束 | 自由组合 |
| 依赖注入 | ✅ 内置 | ❌ 无 | ❌ 无 |
| 学习曲线 | 较陡(需理解 DI、装饰器) | 平缓 | 平缓 |
| 适合项目 | 中大型项目、企业级 | 小型项目、API | 小型项目 |
| 生态 | 官方模块丰富 | 中间件最多 | 中间件较少 |
1.3 核心设计理念
模块化(Module)→ 控制器(Controller)→ 服务(Service) ↑ ↓ 依赖注入(DI)←←←←←←←←←←←←←←←←←←←←←←←←←←←←- 模块化:每个功能是一个 Module,边界清晰
- 依赖注入:Service 由框架自动注入,不需要手动 new
- 装饰器驱动:用装饰器声明路由、权限、参数
- 管道过滤器模式:请求经过 Middleware → Guard → Interceptor → Pipe → Controller → Interceptor → ExceptionFilter
二、项目创建与结构
2.1 环境准备
# 确保 Node.js >= 16node -v# 全局安装 NestJS CLInpm install -g @nestjs/cli# 验证nest --version2.2 创建项目
# 创建新项目nest new my-project# 进入项目目录cd my-project# 启动开发服务npm run start:dev# 默认运行在 http://localhost:30002.3 目录结构
my-project/├── src/│ ├── app.controller.ts # 控制器│ ├── app.controller.spec.ts # 控制器测试│ ├── app.module.ts # 根模块│ ├── app.service.ts # 服务│ └── main.ts # 入口文件├── test/│ ├── app.e2e-spec.ts # E2E 测试│ └── jest-e2e.json├── package.json├── tsconfig.json└── nest-cli.json2.4 入口文件 main.ts
import { NestFactory } from '@nestjs/core';import { AppModule } from './app.module';async function bootstrap() { // 创建应用实例 const app = await NestFactory.create(AppModule); // 启用 CORS app.enableCors(); // 全局前缀 app.setGlobalPrefix('api'); // 启动服务 await app.listen(3000);}bootstrap();2.5 常用 CLI 命令
# 生成资源(自动创建 Controller + Service + Module + DTO + Entity)nest generate resource users# 简写:nest g res users# 单独生成nest g controller users # 控制器nest g service users # 服务nest g module users # 模块nest g middleware logger # 中间件nest g guard auth # 守卫nest g interceptor logging # 拦截器nest g pipe validation # 管道nest g filter http-exception # 异常过滤器三、核心概念
3.1 模块(Module)
模块是组织应用结构的基本单位。
// users/users.module.tsimport { Module } from '@nestjs/common';import { UsersController } from './users.controller';import { UsersService } from './users.service';@Module({ controllers: [UsersController], // 注册控制器 providers: [UsersService], // 注册服务(可被注入) exports: [UsersService], // 导出(其他模块可引入使用)})export class UsersModule {}在根模块中引入:
// app.module.tsimport { Module } from '@nestjs/common';import { UsersModule } from './users/users.module';@Module({ imports: [UsersModule], // 引入子模块})export class AppModule {}全局模块(无需在每个模块中 import):
import { Global, Module } from '@nestjs/common';@Global()@Module({ providers: [ConfigService], exports: [ConfigService],})export class ConfigModule {}3.2 控制器(Controller)
控制器负责接收请求、返回响应。
import { Controller, Get, Post, Body, Param, Query } from '@nestjs/common';import { UsersService } from './users.service';import { CreateUserDto } from './dto/create-user.dto';@Controller('users')export class UsersController { // 依赖注入:构造函数注入 constructor(private readonly usersService: UsersService) {} @Get() findAll(@Query('page') page: number = 1, @Query('size') size: number = 10) { return this.usersService.findAll(page, size); } @Get(':id') findOne(@Param('id') id: string) { return this.usersService.findOne(+id); } @Post() create(@Body() createUserDto: CreateUserDto) { return this.usersService.create(createUserDto); } @Put(':id') update(@Param('id') id: string, @Body() updateUserDto: UpdateUserDto) { return this.usersService.update(+id, updateUserDto); } @Delete(':id') remove(@Param('id') id: string) { return this.usersService.remove(+id); }}3.3 服务(Provider / Service)
服务负责处理业务逻辑,可以被注入到控制器或其他服务中。
import { Injectable } from '@nestjs/common';@Injectable()export class UsersService { private users: User[] = []; findAll(page: number, size: number) { const start = (page - 1) * size; return this.users.slice(start, start + size); } findOne(id: number) { const user = this.users.find(u => u.id === id); if (!user) { throw new NotFoundException(`User #${id} not found`); } return user; } create(createUserDto: CreateUserDto) { const user = { id: Date.now(), ...createUserDto }; this.users.push(user); return user; }}3.4 依赖注入(DI)
NestJS 的核心特性——通过 TypeScript 的类型系统自动注入依赖。
// 方式 1:构造函数注入(推荐)@Controller('users')export class UsersController { constructor(private readonly usersService: UsersService) {} // NestJS 根据类型自动注入 UsersService}// 方式 2:属性注入@Controller('users')export class UsersController { @Inject(UsersService) private readonly usersService: UsersService;}// 自定义 Provider@Module({ providers: [ { provide: 'CONFIG', useValue: { apikey: 'xxx' }, // 注入常量值 }, { provide: 'ASYNC_CONNECTION', useFactory: async () => { const connection = await createConnection(); return connection; // 异步工厂 }, }, ],})export class AppModule {}// 使用自定义 Provider@Injectable()export class SomeService { constructor(@Inject('CONFIG') private config: any) {}}四、装饰器与路由
4.1 路由装饰器
| 装饰器 | 说明 | 示例 |
|---|---|---|
@Controller('users') |
控制器前缀 | /users |
@Get() |
GET 请求 | @Get('list') → /users/list |
@Post() |
POST 请求 | |
@Put() |
PUT 请求 | |
@Patch() |
PATCH 请求 | |
@Delete() |
DELETE 请求 | |
@All() |
所有方法 |
4.2 参数装饰器
@Get(':id')findOne( @Param('id') id: string, // 路径参数 /users/123 @Query('name') name?: string, // 查询参数 ?name=xxx @Body() body: any, // 请求体 @Headers('authorization') token: string, // 请求头 @Req() req: Request, // 完整请求对象 @Res() res: Response, // 完整响应对象 @Session() session: any, // Session @Ip() ip: string, // 客户端 IP) { return { id, name, body, token };}4.3 自定义参数装饰器
// 创建装饰器:从请求中提取当前用户import { createParamDecorator, ExecutionContext } from '@nestjs/common';export const CurrentUser = createParamDecorator( (data: unknown, ctx: ExecutionContext) => { const request = ctx.switchToHttp().getRequest(); return request.user; // 返回当前登录用户 },);// 使用@Get('profile')getProfile(@CurrentUser() user: User) { return user;}五、DTO 与数据验证
5.1 安装验证依赖
npm install class-validator class-transformer5.2 定义 DTO
// dto/create-user.dto.tsimport { IsString, IsEmail, IsInt, MinLength, MaxLength, IsOptional } from 'class-validator';export class CreateUserDto { @IsString() @MinLength(2, { message: '用户名至少 2 个字符' }) @MaxLength(20, { message: '用户名最多 20 个字符' }) username: string; @IsEmail({}, { message: '邮箱格式不正确' }) email: string; @IsString() @MinLength(6, { message: '密码至少 6 位' }) password: string; @IsOptional() @IsInt() age?: number;}5.3 启用全局验证管道
// main.tsimport { ValidationPipe } from '@nestjs/common';async function bootstrap() { const app = await NestFactory.create(AppModule); app.useGlobalPipes( new ValidationPipe({ whitelist: true, // 自动去除 DTO 中未定义的属性 forbidNonWhitelisted: true, // 出现未定义属性时报错 transform: true, // 自动类型转换(字符串 → 数字等) disableErrorMessages: false, // 生产环境可设为 true }), ); await app.listen(3000);}bootstrap();5.4 验证效果
# 请求POST /api/users{ "username": "a", # 太短,验证失败 "email": "not-email", # 格式不对,验证失败 "password": "123", # 太短,验证失败 "hack": "xxx" # 未定义属性,被移除}# 响应(自动返回 400){ "statusCode": 400, "message": [ "用户名至少 2 个字符", "邮箱格式不正确", "密码至少 6 位" ], "error": "Bad Request"}第二部分:进阶
六、中间件与管道
6.1 中间件(Middleware)
中间件在路由处理之前执行,类似 Express 中间件。
// logger.middleware.tsimport { Injectable, NestMiddleware } from '@nestjs/common';import { Request, Response, NextFunction } from 'express';@Injectable()export class LoggerMiddleware implements NestMiddleware { use(req: Request, res: Response, next: NextFunction) { console.log(`[${new Date().toISOString()}] ${req.method} ${req.url}`); next(); }}// 在模块中注册@Module({ controllers: [UsersController], providers: [UsersService],})export class UsersModule implements NestModule { configure(consumer: MiddlewareConsumer) { consumer .apply(LoggerMiddleware) .forRoutes('users'); // 或 forRoutes(UsersController) }}函数式中间件(更简洁):
// logger.middleware.tsexport function logger(req: Request, res: Response, next: NextFunction) { console.log(`${req.method} ${req.url}`); next();}// 使用consumer.apply(logger).forRoutes('users');6.2 管道(Pipe)
管道用于数据转换和验证。
| 内置管道 | 说明 |
|---|---|
ValidationPipe |
验证 DTO |
ParseIntPipe |
字符串转数字 |
ParseFloatPipe |
字符串转浮点数 |
ParseBoolPipe |
字符串转布尔值 |
ParseUUIDPipe |
验证 UUID |
DefaultValuePipe |
设置默认值 |
@Get(':id')findOne( @Param('id', ParseIntPipe) id: number, // 自动转数字,失败返回 400) { return this.usersService.findOne(id);}// 带自定义错误信息@Get(':id')findOne( @Param('id', new ParseIntPipe({ error: () => new BadRequestException('ID 必须是数字') })) id: number,) {}// 多个管道组合 + 默认值@Get()findAll( @Query('page', new DefaultValuePipe(1), ParseIntPipe) page: number, @Query('size', new DefaultValuePipe(10), ParseIntPipe) size: number,) {}自定义管道:
// pipes/validate-status.pipe.tsimport { PipeTransform, Injectable, BadRequestException } from '@nestjs/common';@Injectable()export class ValidateStatusPipe implements PipeTransform { transform(value: any) { const validStatuses = ['active', 'inactive', 'banned']; if (!validStatuses.includes(value)) { throw new BadRequestException(`状态必须是: ${validStatuses.join(', ')}`); } return value; }}七、守卫与拦截器
7.1 守卫(Guard)
守卫用于权限控制,决定请求是否继续处理。
// auth.guard.tsimport { Injectable, CanActivate, ExecutionContext } from '@nestjs/common';@Injectable()export class AuthGuard implements CanActivate { canActivate(context: ExecutionContext): boolean { const request = context.switchToHttp().getRequest(); const token = request.headers.authorization; if (!token) { return false; // 无 token,拒绝访问 } // 验证 token... return true; }}// 控制器级使用@UseGuards(AuthGuard)@Controller('users')export class UsersController {}// 方法级使用@Get('profile')@UseGuards(AuthGuard)getProfile() {}// 全局使用app.useGlobalGuards(new AuthGuard());// 或在模块中注册@Module({ providers: [ { provide: APP_GUARD, useClass: AuthGuard, }, ],})7.2 拦截器(Interceptor)
拦截器用于在请求前后做处理,类似 AOP(面向切面编程)。
// logging.interceptor.tsimport { Injectable, NestInterceptor, ExecutionContext, CallHandler } from '@nestjs/common';import { Observable } from 'rxjs';import { tap } from 'rxjs/operators';@Injectable()export class LoggingInterceptor implements NestInterceptor { intercept(context: ExecutionContext, next: CallHandler): Observable<any> { const now = Date.now(); console.log('请求前...'); return next .handle() // 调用路由处理方法 .pipe( tap(() => console.log(`请求后... 耗时 ${Date.now() - now}ms`)), ); }}// 响应转换拦截器(统一返回格式)@Injectable()export class TransformInterceptor implements NestInterceptor { intercept(context: ExecutionContext, next: CallHandler) { return next.handle().pipe( map(data => ({ code: 200, message: 'success', data, })), ); }}// 超时拦截器@Injectable()export class TimeoutInterceptor implements NestInterceptor { intercept(context: ExecutionContext, next: CallHandler) { return next.handle().pipe( timeout(5000), // 5 秒超时 catchError(err => { if (err instanceof TimeoutError) { throw new RequestTimeoutException('请求超时'); } throw err; }), ); }}7.3 请求生命周期
请求进来 ↓中间件(Middleware) ↓守卫(Guard)—— 权限判断 ↓拦截器(Interceptor)—— 请求前 ↓管道(Pipe)—— 参数转换/验证 ↓控制器方法(Controller) ↓拦截器(Interceptor)—— 请求后 ↓异常过滤器(Exception Filter)—— 如果有异常 ↓响应返回八、异常过滤器
8.1 内置异常
throw new BadRequestException('参数错误'); // 400throw new UnauthorizedException('未授权'); // 401throw new ForbiddenException('禁止访问'); // 403throw new NotFoundException('资源不存在'); // 404throw new ConflictException('冲突'); // 409throw new UnprocessableEntityException(); // 422throw new InternalServerErrorException(); // 5008.2 自定义异常
// exceptions/business.exception.tsexport class BusinessException extends HttpException { constructor(code: number, message: string) { super({ code, message }, HttpStatus.OK); // HTTP 200,但业务码非 0 }}// 使用throw new BusinessException(40001, '用户名已存在');8.3 全局异常过滤器
// filters/http-exception.filter.tsimport { ExceptionFilter, Catch, ArgumentsHost, HttpException, HttpStatus } from '@nestjs/common';import { Request, Response } from 'express';@Catch() // 捕获所有异常export class AllExceptionsFilter implements ExceptionFilter { catch(exception: unknown, host: ArgumentsHost) { const ctx = host.switchToHttp(); const response = ctx.getResponse<Response>(); const request = ctx.getRequest<Request>(); let status = HttpStatus.INTERNAL_SERVER_ERROR; let message = '服务器内部错误'; if (exception instanceof HttpException) { status = exception.getStatus(); const res = exception.getResponse(); message = typeof res === 'string' ? res : (res as any).message; } // 统一返回格式 response.status(status).json({ code: status, message: Array.isArray(message) ? message[0] : message, data: null, timestamp: new Date().toISOString(), path: request.url, }); }}// 全局注册// main.tsapp.useGlobalFilters(new AllExceptionsFilter());// 或在模块中注册@Module({ providers: [ { provide: APP_FILTER, useClass: AllExceptionsFilter }, ],})九、数据库集成
9.1 TypeORM 集成
npm install @nestjs/typeorm typeorm mysql2// app.module.tsimport { TypeOrmModule } from '@nestjs/typeorm';@Module({ imports: [ TypeOrmModule.forRoot({ type: 'mysql', host: 'localhost', port: 3306, username: 'root', password: 'password', database: 'test_db', entities: [__dirname + '/**/*.entity{.ts,.js}'], synchronize: true, // 开发环境自动同步表结构,生产环境必须 false logging: true, // 打印 SQL }), ],})export class AppModule {}9.2 实体定义
// users/user.entity.tsimport { Entity, Column, PrimaryGeneratedColumn, CreateDateColumn, UpdateDateColumn } from 'typeorm';@Entity('users')export class User { @PrimaryGeneratedColumn() id: number; @Column({ length: 50, unique: true }) username: string; @Column({ select: false }) // 查询时不返回密码 password: string; @Column({ length: 100 }) email: string; @Column({ type: 'enum', enum: ['active', 'inactive', 'banned'], default: 'active' }) status: string; @Column({ type: 'int', default: 0 }) age: number; @CreateDateColumn() createdAt: Date; @UpdateDateColumn() updatedAt: Date;}9.3 在 Service 中使用 Repository
// users/users.service.tsimport { Injectable } from '@nestjs/common';import { InjectRepository } from '@nestjs/typeorm';import { Repository } from 'typeorm';import { User } from './user.entity';@Injectable()export class UsersService { constructor( @InjectRepository(User) private userRepository: Repository<User>, ) {} async findAll() { return this.userRepository.find(); } async findOne(id: number) { return this.userRepository.findOne({ where: { id } }); } async create(createUserDto: CreateUserDto) { const user = this.userRepository.create(createUserDto); return this.userRepository.save(user); } async update(id: number, updateUserDto: UpdateUserDto) { await this.userRepository.update(id, updateUserDto); return this.userRepository.findOne({ where: { id } }); } async remove(id: number) { await this.userRepository.delete(id); } // 使用 QueryBuilder 复杂查询 async findActiveUsers() { return this.userRepository .createQueryBuilder('user') .where('user.status = :status', { status: 'active' }) .orderBy('user.createdAt', 'DESC') .getMany(); }}9.4 Prisma 集成(替代方案)
npm install @prisma/clientnpm install -D prismanpx prisma init// prisma/schema.prismagenerator client { provider = "prisma-client-js"}datasource db { provider = "mysql" url = env("DATABASE_URL")}model User { id Int @id @default(autoincrement()) username String @unique password String email String status String @default("active") createdAt DateTime @default(now()) updatedAt DateTime @updatedAt}// prisma.service.tsimport { Injectable, OnModuleInit } from '@nestjs/common';import { PrismaClient } from '@prisma/client';@Injectable()export class PrismaService extends PrismaClient implements OnModuleInit { async onModuleInit() { await this.$connect(); }}// users.service.ts@Injectable()export class UsersService { constructor(private prisma: PrismaService) {} findAll() { return this.prisma.user.findMany(); } create(data: CreateUserDto) { return this.prisma.user.create({ data }); }}十、认证与授权
10.1 JWT 认证
npm install @nestjs/jwt @nestjs/passport passport passport-jwt bcryptnpm install -D @types/passport-jwt10.2 Auth 模块完整实现
// auth/auth.module.tsimport { Module } from '@nestjs/common';import { JwtModule } from '@nestjs/jwt';import { PassportModule } from '@nestjs/passport';import { JwtStrategy } from './jwt.strategy';import { AuthService } from './auth.service';import { AuthController } from './auth.controller';import { UsersModule } from '../users/users.module';@Module({ imports: [ UsersModule, PassportModule, JwtModule.register({ secret: 'your-secret-key', // 生产环境用环境变量 signOptions: { expiresIn: '7d' }, }), ], controllers: [AuthController], providers: [AuthService, JwtStrategy], exports: [AuthService],})export class AuthModule {}// auth/auth.service.tsimport { Injectable, UnauthorizedException } from '@nestjs/common';import { JwtService } from '@nestjs/jwt';import * as bcrypt from 'bcrypt';import { UsersService } from '../users/users.service';@Injectable()export class AuthService { constructor( private usersService: UsersService, private jwtService: JwtService, ) {} async register(registerDto: RegisterDto) { // 加密密码 const hashedPassword = await bcrypt.hash(registerDto.password, 10); const user = await this.usersService.create({ ...registerDto, password: hashedPassword, }); // 生成 token const token = this.jwtService.sign({ sub: user.id, username: user.username }); return { user, token }; } async login(loginDto: LoginDto) { const user = await this.usersService.findByUsername(loginDto.username); if (!user) { throw new UnauthorizedException('用户名或密码错误'); } const isPasswordValid = await bcrypt.compare(loginDto.password, user.password); if (!isPasswordValid) { throw new UnauthorizedException('用户名或密码错误'); } const token = this.jwtService.sign({ sub: user.id, username: user.username }); return { user, token }; }}// auth/jwt.strategy.tsimport { ExtractJwt, Strategy } from 'passport-jwt';import { PassportStrategy } from '@nestjs/passport';import { Injectable } from '@nestjs/common';@Injectable()export class JwtStrategy extends PassportStrategy(Strategy) { constructor() { super({ jwtFromRequest: ExtractJwt.fromAuthHeaderAsBearerToken(), ignoreExpiration: false, secretOrKey: 'your-secret-key', }); } // token 解析后的 payload 会传给这里 async validate(payload: any) { return { userId: payload.sub, username: payload.username }; }}// auth/auth.controller.tsimport { Controller, Post, Body } from '@nestjs/common';import { AuthService } from './auth.service';@Controller('auth')export class AuthController { constructor(private authService: AuthService) {} @Post('register') register(@Body() registerDto: RegisterDto) { return this.authService.register(registerDto); } @Post('login') login(@Body() loginDto: LoginDto) { return this.authService.login(loginDto); }}10.3 JWT 守卫
// auth/jwt-auth.guard.tsimport { Injectable } from '@nestjs/common';import { AuthGuard } from '@nestjs/passport';@Injectable()export class JwtAuthGuard extends AuthGuard('jwt') {}// 使用@Get('profile')@UseGuards(JwtAuthGuard)getProfile(@Request() req) { return req.user; // JwtStrategy.validate() 返回的对象}10.4 角色权限
// roles.decorator.tsimport { SetMetadata } from '@nestjs/common';export const Roles = (...roles: string[]) => SetMetadata('roles', roles);// roles.guard.tsimport { Injectable, CanActivate, ExecutionContext } from '@nestjs/common';import { Reflector } from '@nestjs/core';@Injectable()export class RolesGuard implements CanActivate { constructor(private reflector: Reflector) {} canActivate(context: ExecutionContext): boolean { const requiredRoles = this.reflector.get<string[]>('roles', context.getHandler()); if (!requiredRoles) { return true; // 没有设置角色要求,放行 } const { user } = context.switchToHttp().getRequest(); return requiredRoles.some(role => user.roles?.includes(role)); }}// 使用@Get('admin')@UseGuards(JwtAuthGuard, RolesGuard)@Roles('admin')getAdminData() { return { message: '管理员数据' };}十一、Swagger 文档
npm install @nestjs/swagger// main.tsimport { SwaggerModule, DocumentBuilder } from '@nestjs/swagger';async function bootstrap() { const app = await NestFactory.create(AppModule); const config = new DocumentBuilder() .setTitle('Travel API') .setDescription('旅游网站 API 文档') .setVersion('1.0') .addBearerAuth() // 启用 JWT 认证 .build(); const document = SwaggerModule.createDocument(app, config); SwaggerModule.setup('api/docs', app, document); await app.listen(3000);}// 在 DTO 和 Controller 中添加文档注解import { ApiTags, ApiOperation, ApiResponse, ApiProperty } from '@nestjs/swagger';@ApiTags('用户管理')@Controller('users')export class UsersController { @ApiOperation({ summary: '获取用户列表' }) @ApiResponse({ status: 200, description: '成功' }) @Get() findAll() {}}// DTOexport class CreateUserDto { @ApiProperty({ example: '张三', description: '用户名' }) @IsString() username: string; @ApiProperty({ example: 'zhangsan@example.com', description: '邮箱' }) @IsEmail() email: string;}访问 http://localhost:3000/api/docs 即可看到自动生成的 API 文档。
十二、配置管理
npm install @nestjs/config# .envDATABASE_HOST=localhostDATABASE_PORT=3306DATABASE_USER=rootDATABASE_PASSWORD=123456DATABASE_NAME=travel_dbJWT_SECRET=your-secret-key// app.module.tsimport { ConfigModule } from '@nestjs/config';@Module({ imports: [ ConfigModule.forRoot({ isGlobal: true, // 全局可用,无需每个模块都 import envFilePath: ['.env.local', '.env'], // 优先级从高到低 }), TypeOrmModule.forRootAsync({ imports: [ConfigModule], inject: [ConfigService], useFactory: (configService: ConfigService) => ({ type: 'mysql', host: configService.get('DATABASE_HOST'), port: +configService.get('DATABASE_PORT'), username: configService.get('DATABASE_USER'), password: configService.get('DATABASE_PASSWORD'), database: configService.get('DATABASE_NAME'), entities: [__dirname + '/**/*.entity{.ts,.js}'], synchronize: configService.get('NODE_ENV') !== 'production', }), }), ],})export class AppModule {}// 使用@Injectable()export class SomeService { constructor(private configService: ConfigService) {} getJwtSecret() { return this.configService.get<string>('JWT_SECRET'); }}第三部分:实战
十三、完整项目结构
src/├── common/ # 公共模块│ ├── decorators/ # 自定义装饰器│ ├── filters/ # 异常过滤器│ ├── guards/ # 守卫│ ├── interceptors/ # 拦截器│ ├── middleware/ # 中间件│ ├── pipes/ # 管道│ └── dto/ # 公共 DTO(分页、响应格式等)├── config/ # 配置│ ├── database.config.ts│ ├── jwt.config.ts│ └── index.ts├── modules/ # 业务模块│ ├── auth/│ │ ├── auth.module.ts│ │ ├── auth.controller.ts│ │ ├── auth.service.ts│ │ ├── jwt.strategy.ts│ │ ├── jwt-auth.guard.ts│ │ └── dto/│ ├── users/│ │ ├── users.module.ts│ │ ├── users.controller.ts│ │ ├── users.service.ts│ │ ├── user.entity.ts│ │ └── dto/│ ├── routes/ # 旅游线路│ └── orders/ # 订单├── app.module.ts└── main.ts十四、实战:用户模块完整实现
14.1 公共响应格式
// common/dto/response.dto.tsexport class ResponseDto<T = any> { code: number; message: string; data: T; constructor(code: number, message: string, data: T) { this.code = code; this.message = message; this.data = data; } static success<T>(data: T, message = 'success') { return new ResponseDto(200, message, data); } static error(code: number, message: string) { return new ResponseDto(code, message, null); }}// common/interceptors/transform.interceptor.ts@Injectable()export class TransformInterceptor<T> implements NestInterceptor<T, ResponseDto<T>> { intercept(context: ExecutionContext, next: CallHandler): Observable<ResponseDto<T>> { return next.handle().pipe( map(data => ResponseDto.success(data)), ); }}// main.ts 全局启用app.useGlobalInterceptors(new TransformInterceptor());14.2 分页 DTO
// common/dto/pagination.dto.tsimport { IsOptional, IsInt, Min, Max } from 'class-validator';import { Type } from 'class-transformer';import { ApiPropertyOptional } from '@nestjs/swagger';export class PaginationDto { @ApiPropertyOptional({ default: 1 }) @IsOptional() @Type(() => Number) @IsInt() @Min(1) page?: number = 1; @ApiPropertyOptional({ default: 10, maximum: 100 }) @IsOptional() @Type(() => Number) @IsInt() @Min(1) @Max(100) pageSize?: number = 10; get offset(): number { return (this.page - 1) * this.pageSize; }}14.3 用户服务
// modules/users/users.service.ts@Injectable()export class UsersService { constructor( @InjectRepository(User) private userRepository: Repository<User>, ) {} async findAll(paginationDto: PaginationDto) { const { page, pageSize, offset } = paginationDto; const [list, total] = await this.userRepository.findAndCount({ select: ['id', 'username', 'email', 'status', 'createdAt'], order: { createdAt: 'DESC' }, skip: offset, take: pageSize, }); return { list, total, page, pageSize, totalPages: Math.ceil(total / pageSize), }; } async findOne(id: number) { const user = await this.userRepository.findOne({ where: { id }, select: ['id', 'username', 'email', 'status', 'createdAt'], }); if (!user) { throw new NotFoundException(`用户 #${id} 不存在`); } return user; } async findByUsername(username: string) { return this.userRepository.findOne({ where: { username } }); } async create(createUserDto: CreateUserDto) { const exists = await this.findByUsername(createUserDto.username); if (exists) { throw new ConflictException('用户名已存在'); } const user = this.userRepository.create(createUserDto); return this.userRepository.save(user); } async update(id: number, updateUserDto: UpdateUserDto) { const user = await this.findOne(id); Object.assign(user, updateUserDto); return this.userRepository.save(user); } async remove(id: number) { const result = await this.userRepository.delete(id); if (result.affected === 0) { throw new NotFoundException(`用户 #${id} 不存在`); } }}十五、文件上传
// main.ts// 静态文件服务app.useStaticAssets(join(__dirname, '..', 'uploads'), { prefix: '/uploads/',});// modules/upload/upload.controller.tsimport { Controller, Post, UseInterceptors } from '@nestjs/common';import { FileInterceptor } from '@nestjs/platform-express';import { diskStorage } from 'multer';import { extname } from 'path';@Controller('upload')export class UploadController { @Post('file') @UseInterceptors( FileInterceptor('file', { storage: diskStorage({ destination: './uploads', filename: (req, file, cb) => { const randomName = Array(32) .fill(null) .map(() => Math.round(Math.random() * 16).toString(16)) .join(''); cb(null, `${randomName}${extname(file.originalname)}`); }, }), limits: { fileSize: 5 * 1024 * 1024, // 5MB }, fileFilter: (req, file, cb) => { const allowedTypes = /jpeg|jpg|png|gif|webp/; const extnameOk = allowedTypes.test(extname(file.originalname).toLowerCase()); const mimetypeOk = allowedTypes.test(file.mimetype); if (extnameOk && mimetypeOk) { cb(null, true); } else { cb(new BadRequestException('只支持 jpg/png/gif/webp 格式'), false); } }, }), ) uploadFile(@UploadedFile() file: Express.Multer.File) { if (!file) { throw new BadRequestException('请上传文件'); } return { url: `/uploads/${file.filename}`, filename: file.filename, size: file.size, }; }}十六、WebSocket 实时通信
npm install @nestjs/websockets @nestjs/platform-socket.io socket.io// chat/chat.gateway.tsimport { WebSocketGateway, SubscribeMessage, MessageBody, ConnectedSocket, OnGatewayConnection, OnGatewayDisconnect,} from '@nestjs/websockets';import { Server, Socket } from 'socket.io';@WebSocketGateway({ cors: { origin: '*' }, namespace: 'chat',})export class ChatGateway implements OnGatewayConnection, OnGatewayDisconnect { server: Server; handleConnection(client: Socket) { console.log(`客户端连接: ${client.id}`); // 可以从握手信息中获取 token 验证身份 } handleDisconnect(client: Socket) { console.log(`客户端断开: ${client.id}`); } // 加入房间 @SubscribeMessage('joinRoom') handleJoinRoom(@MessageBody() room: string, @ConnectedSocket() client: Socket) { client.join(room); client.to(room).emit('userJoined', `用户 ${client.id} 加入了房间`); } // 发送消息 @SubscribeMessage('sendMessage') handleMessage( @MessageBody() data: { room: string; message: string }, @ConnectedSocket() client: Socket, ) { // 广播给房间内所有人(除了发送者) client.to(data.room).emit('newMessage', { sender: client.id, message: data.message, timestamp: new Date().toISOString(), }); return { status: 'ok' }; // 给发送者的确认 }}前端使用:
import { io } from 'socket.io-client';const socket = io('http://localhost:3000/chat');socket.emit('joinRoom', 'room-001');socket.on('newMessage', (data) => { console.log('收到消息:', data);});socket.emit('sendMessage', { room: 'room-001', message: '你好' });十七、定时任务
npm install @nestjs/schedule// app.module.tsimport { ScheduleModule } from '@nestjs/schedule';@Module({ imports: [ScheduleModule.forRoot()],})// tasks/tasks.service.tsimport { Injectable, Logger } from '@nestjs/common';import { Cron, CronExpression, Interval, Timeout } from '@nestjs/schedule';@Injectable()export class TasksService { private readonly logger = new Logger(TasksService.name); // 每天 0 点执行 @Cron('0 0 * * *') handleDailyTask() { this.logger.log('每日任务执行'); } // 每周一早上 9 点 @Cron('0 9 * * 1') handleWeeklyReport() { this.logger.log('生成周报'); } // 每 5 分钟 @Cron(CronExpression.EVERY_5_MINUTES) handleHealthCheck() { this.logger.log('健康检查'); } // 每 10 秒 @Interval(10000) handleInterval() { this.logger.log('每 10 秒执行一次'); } // 应用启动后 5 秒执行一次 @Timeout(5000) handleTimeout() { this.logger.log('启动后 5 秒执行'); }}Cron 表达式:秒 分 时 日 月 星期
* * * * * *│ │ │ │ │ ││ │ │ │ │ └─ 星期 (0-7, 0和7都是周日)│ │ │ │ └─── 月 (1-12)│ │ │ └───── 日 (1-31)│ │ └─────── 时 (0-23)│ └───────── 分 (0-59)└─────────── 秒 (0-59)十八、部署与上线
18.1 打包
# 构建npm run build# 产物在 dist/ 目录ls -la dist/18.2 PM2 部署
# 安装 PM2npm install -g pm2# 启动pm2 start dist/main.js --name travel-api -i max# 常用命令pm2 list # 查看所有进程pm2 logs travel-api # 查看日志pm2 restart travel-api # 重启pm2 stop travel-api # 停止pm2 delete travel-api # 删除pm2 monit # 监控面板# 开机自启pm2 startuppm2 save18.3 Docker 部署
# DockerfileFROM node:18-alpineWORKDIR /app# 安装依赖COPY package*.json ./RUN npm install --production# 复制代码COPY . .# 构建RUN npm run build# 暴露端口EXPOSE 3000# 启动CMD ["node", "dist/main.js"]# docker-compose.ymlversion: '3.8'services: api: build: . ports: - "3000:3000" environment: - NODE_ENV=production - DATABASE_HOST=mysql - REDIS_HOST=redis depends_on: - mysql - redis restart: always mysql: image: mysql:8 environment: MYSQL_ROOT_PASSWORD: rootpassword MYSQL_DATABASE: travel_db volumes: - mysql_data:/var/lib/mysql ports: - "3306:3306" redis: image: redis:7-alpine ports: - "6379:6379"volumes: mysql_data:docker-compose up -d18.4 Nginx 反向代理
server { listen 80; server_name api.example.com; location / { proxy_pass http://127.0.0.1:3000; proxy_set_header Host $host; proxy_set_header X-Real-IP $remote_addr; proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; # WebSocket 支持 proxy_http_version 1.1; proxy_set_header Upgrade $http_upgrade; proxy_set_header Connection "upgrade"; } # 静态文件 location /uploads/ { alias /www/wwwroot/api/uploads/; }}十九、常见面试题
Q1:NestJS 和 Express 的区别?
- NestJS 是基于 Express(或 Fastify)的上层框架
- NestJS 提供了模块化、依赖注入、装饰器等架构能力
- Express 更自由,NestJS 更规范
- NestJS 适合中大型项目,Express 适合小型项目
Q2:NestJS 的依赖注入原理?
- 通过 TypeScript 的类型反射机制
@Injectable()标记的类会被注册为 Provider- 框架根据构造函数参数类型自动创建并注入实例
- 底层使用自己的 DI 容器(类似 Angular)
Q3:Provider 的三种注册方式?
// 1. 直接用类providers: [UsersService]// 2. useValue(注入常量)providers: [{ provide: 'CONFIG', useValue: {} }]// 3. useFactory(异步创建)providers: [{ provide: 'DB_CONNECTION', useFactory: async () => await createConnection(),}]Q4:Middleware、Guard、Pipe、Interceptor 的执行顺序?
Middleware → Guard → Interceptor(前) → Pipe → Controller → Interceptor(后) → ExceptionFilterQ5:Guard 和 Middleware 的区别?
- Middleware 不知道下一个处理者是谁
- Guard 知道下一个要执行什么(ExecutionContext),适合做权限控制
- Guard 返回 false 直接拦截请求
Q6:Interceptor 能做什么?
- 请求前后做处理(日志、计时)
- 响应转换(统一返回格式)
- 缓存
- 超时控制
- 异常映射
Q7:全局管道、守卫、过滤器怎么注册?
// 方式 1:main.tsapp.useGlobalPipes(new ValidationPipe());app.useGlobalFilters(new AllExceptionsFilter());app.useGlobalGuards(new AuthGuard());// 方式 2:模块中(推荐,可注入依赖){ provide: APP_PIPE, useClass: ValidationPipe }{ provide: APP_FILTER, useClass: AllExceptionsFilter }{ provide: APP_GUARD, useClass: AuthGuard }Q8:TypeORM 的 synchronize 什么时候可以用?
- 开发环境可以(true),自动同步实体到表结构
- 生产环境绝对不能(false),会导致数据丢失
- 生产环境用 migration(迁移脚本)
Q9:NestJS 如何实现微服务?
// 微服务方式启动const app = await NestFactory.createMicroservice(AppModule, { transport: Transport.RMQ, options: { urls: ['amqp://localhost:5672'], queue: 'cats_queue' },});app.listen();Q10:NestJS 的 DTO 为什么要用 class 而不是 interface?
- interface 是 TypeScript 编译时的,运行时不存在
- class-validator 需要在运行时读取类型信息
- class 在运行时存在,才能做验证
二十、最佳实践
- 模块划分清晰:每个业务一个 Module,Module 之间通过 exports/imports 通信
- DTO 严格验证:所有输入用 class-validator 验证,不要相信前端
- 统一响应格式:用拦截器统一返回
{ code, message, data } - 统一异常处理:用全局异常过滤器统一处理
- 配置环境隔离:用 @nestjs/config + .env 文件,不硬编码
- 密码加密:用 bcrypt,不要用 MD5/SHA
- 日志规范:用 NestJS 内置 Logger 或 winston,不要用 console.log
- 生产关闭 synchronize:用 migration 管理表结构变更
- API 文档自动化:用 Swagger,保持代码和文档同步
- 测试覆盖:单元测试用 Jest,E2E 测试用 supertest
总结
NestJS 的核心可以归纳为三层:
- 基础层:Module / Controller / Provider / DI —— 框架的骨架
- 能力层:Middleware / Guard / Pipe / Interceptor / Filter —— 请求处理管道
- 生态层:TypeORM / JWT / Swagger / Config / WebSocket / Schedule —— 开箱即用的模块
掌握这三层,就能用 NestJS 高效构建企业级 Node.js 后端服务。
NestJS 的学习曲线比 Express 陡,但一旦理解了依赖注入和装饰器模式,开发效率会远高于裸写 Express。适合中大型项目长期维护。