全栈手账 订阅 RSS

NestJS 知识总结(基础 + 进阶 + 实战)

NestJS 是基于 TypeScript 的 Node.js 服务端框架,融合 OOP、FP 与响应式编程,受 Angular 启发。本文覆盖模块、控制器、依赖注入、数据库、认证与部署。

读了 75 分钟 更新于 8 月 10 日

第一部分:基础

一、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 环境准备

bash
# 确保 Node.js >= 16node -v# 全局安装 NestJS CLInpm install -g @nestjs/cli# 验证nest --version

2.2 创建项目

bash
# 创建新项目nest new my-project# 进入项目目录cd my-project# 启动开发服务npm run start:dev# 默认运行在 http://localhost:3000

2.3 目录结构

code
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.json

2.4 入口文件 main.ts

typescript
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 命令

bash
# 生成资源(自动创建 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)

模块是组织应用结构的基本单位。

typescript
// 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 {}

在根模块中引入:

typescript
// app.module.tsimport { Module } from '@nestjs/common';import { UsersModule } from './users/users.module';@Module({  imports: [UsersModule],  // 引入子模块})export class AppModule {}

全局模块(无需在每个模块中 import):

typescript
import { Global, Module } from '@nestjs/common';@Global()@Module({  providers: [ConfigService],  exports: [ConfigService],})export class ConfigModule {}

3.2 控制器(Controller)

控制器负责接收请求、返回响应。

typescript
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)

服务负责处理业务逻辑,可以被注入到控制器或其他服务中。

typescript
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 的类型系统自动注入依赖。

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 参数装饰器

typescript
@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 自定义参数装饰器

typescript
// 创建装饰器:从请求中提取当前用户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 安装验证依赖

bash
npm install class-validator class-transformer

5.2 定义 DTO

typescript
// 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 启用全局验证管道

typescript
// 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 验证效果

bash
# 请求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 中间件。

typescript
// 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)  }}

函数式中间件(更简洁):

typescript
// 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 设置默认值
typescript
@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,) {}

自定义管道:

typescript
// 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)

守卫用于权限控制,决定请求是否继续处理。

typescript
// 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(面向切面编程)。

typescript
// 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 内置异常

typescript
throw new BadRequestException('参数错误');         // 400throw new UnauthorizedException('未授权');          // 401throw new ForbiddenException('禁止访问');           // 403throw new NotFoundException('资源不存在');          // 404throw new ConflictException('冲突');               // 409throw new UnprocessableEntityException();           // 422throw new InternalServerErrorException();           // 500

8.2 自定义异常

typescript
// 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 全局异常过滤器

typescript
// 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 集成

bash
npm install @nestjs/typeorm typeorm mysql2
typescript
// 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 实体定义

typescript
// 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

typescript
// 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 集成(替代方案)

bash
npm install @prisma/clientnpm install -D prismanpx prisma init
prisma
// 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}
typescript
// 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 认证

bash
npm install @nestjs/jwt @nestjs/passport passport passport-jwt bcryptnpm install -D @types/passport-jwt

10.2 Auth 模块完整实现

typescript
// 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 {}
typescript
// 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 };  }}
typescript
// 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 };  }}
typescript
// 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 守卫

typescript
// 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 角色权限

typescript
// 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 文档

bash
npm install @nestjs/swagger
typescript
// 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);}
typescript
// 在 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 文档。


十二、配置管理

bash
npm install @nestjs/config
bash
# .envDATABASE_HOST=localhostDATABASE_PORT=3306DATABASE_USER=rootDATABASE_PASSWORD=123456DATABASE_NAME=travel_dbJWT_SECRET=your-secret-key
typescript
// 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');  }}

第三部分:实战

十三、完整项目结构

code
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 公共响应格式

typescript
// 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

typescript
// 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 用户服务

typescript
// 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} 不存在`);    }  }}

十五、文件上传

typescript
// 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 实时通信

bash
npm install @nestjs/websockets @nestjs/platform-socket.io socket.io
typescript
// 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' };  // 给发送者的确认  }}

前端使用:

javascript
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: '你好' });

十七、定时任务

bash
npm install @nestjs/schedule
typescript
// 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 表达式:秒 分 时 日 月 星期

code
* * * * * *│ │ │ │ │ ││ │ │ │ │ └─ 星期 (0-7, 0和7都是周日)│ │ │ │ └─── 月 (1-12)│ │ │ └───── 日 (1-31)│ │ └─────── 时 (0-23)│ └───────── 分 (0-59)└─────────── 秒 (0-59)

十八、部署与上线

18.1 打包

bash
# 构建npm run build# 产物在 dist/ 目录ls -la dist/

18.2 PM2 部署

bash
# 安装 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 save

18.3 Docker 部署

dockerfile
# DockerfileFROM node:18-alpineWORKDIR /app# 安装依赖COPY package*.json ./RUN npm install --production# 复制代码COPY . .# 构建RUN npm run build# 暴露端口EXPOSE 3000# 启动CMD ["node", "dist/main.js"]
yaml
# 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:
bash
docker-compose up -d

18.4 Nginx 反向代理

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 的三种注册方式?

typescript
// 1. 直接用类providers: [UsersService]// 2. useValue(注入常量)providers: [{ provide: 'CONFIG', useValue: {} }]// 3. useFactory(异步创建)providers: [{  provide: 'DB_CONNECTION',  useFactory: async () => await createConnection(),}]

Q4:Middleware、Guard、Pipe、Interceptor 的执行顺序?

code
Middleware → Guard → Interceptor(前) → Pipe → Controller → Interceptor(后) → ExceptionFilter

Q5:Guard 和 Middleware 的区别?

  • Middleware 不知道下一个处理者是谁
  • Guard 知道下一个要执行什么(ExecutionContext),适合做权限控制
  • Guard 返回 false 直接拦截请求

Q6:Interceptor 能做什么?

  • 请求前后做处理(日志、计时)
  • 响应转换(统一返回格式)
  • 缓存
  • 超时控制
  • 异常映射

Q7:全局管道、守卫、过滤器怎么注册?

typescript
// 方式 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 如何实现微服务?

typescript
// 微服务方式启动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 在运行时存在,才能做验证

二十、最佳实践

  1. 模块划分清晰:每个业务一个 Module,Module 之间通过 exports/imports 通信
  2. DTO 严格验证:所有输入用 class-validator 验证,不要相信前端
  3. 统一响应格式:用拦截器统一返回 { code, message, data }
  4. 统一异常处理:用全局异常过滤器统一处理
  5. 配置环境隔离:用 @nestjs/config + .env 文件,不硬编码
  6. 密码加密:用 bcrypt,不要用 MD5/SHA
  7. 日志规范:用 NestJS 内置 Logger 或 winston,不要用 console.log
  8. 生产关闭 synchronize:用 migration 管理表结构变更
  9. API 文档自动化:用 Swagger,保持代码和文档同步
  10. 测试覆盖:单元测试用 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。适合中大型项目长期维护。


前端、后端、部署、客服都是我一个人。写这里的东西都是真踩过的坑,不是抄来的教程。