nest初体验-用nest实现一个简单的CRUD功能

nest简介

Nest是一个用于构建高效、可扩展的Node服务器端应用框架。它默认使用Express作为HTTP服务端框架,但是也可以支持Fastify作为替代,使用TypeScript构建并全面支持 TypeScript,同时仍允许开发者使用纯 JavaScript 编码,融合了 OOP(面向对象编程)、FP(函数式编程)和 FRP(函数响应式编程)等元素。它的核心目标是解决传统 Node.js 框架(如 Express/Koa)缺乏统一架构规范、大型项目代码易混乱的问题。

NestJS 采用了一种清晰的三层结构来组织应用逻辑,也就是MVC结构。

1. 模块 (Modules)

  • 作用: 模块是 NestJS 应用的基本组织单元 。它们是具有 @Module() 装饰器的类,用于将应用的不同功能和特性(如用户管理、订单处理等)逻辑上划分为独立的、可重用的部分。

  • 职责: 模块负责组合 控制器和服务(提供者),并定义一组功能。每个应用至少有一个根模块(Root Module)

  • 关键属性:

    • controllers: 属于本模块的控制器
    • providers: 属于本模块的服务 、仓库、工厂等(统称为提供者)。
    • imports: 导入其他模块,以便使用它们导出的提供者。
    • exports: 导出本模块的提供者,供其他模块导入使用。

2. 控制器 (Controllers)

  • 作用: 控制器负责处理传入的请求 和返回响应。它们作为应用的入口点。
  • 职责: 它们使用 @Controller() 装饰器定义路由路径,并使用 @Get(), @Post(), @Put(), @Delete() 等装饰器来处理特定的 HTTP 方法。控制器通常只包含路由逻辑,并将复杂的业务逻辑委托给服务(提供者)。

3. 提供者 (Providers / Services)

  • 作用: 提供者是 NestJS 中最核心 的概念之一,用于处理业务逻辑和数据持久化。
  • 职责: 它们是可注入(Injectable)的类,通常包含复杂的逻辑、数据库交互、外部 API 调用等。它们使用 @Injectable() 装饰器,并由 NestJS 的依赖注入系统来管理其生命周期和依赖关系。
  • 常见类型: 服务(Services)、仓库(Repositories)、工厂(Factories)、辅助函数等。

架构支撑模块(通过装饰器实现)

这些不是传统意义上的"模块类",而是 NestJS 用来增强功能和控制应用行为的架构组件

模块/概念 装饰器 作用
依赖注入 (DI) @Injectable() 管理提供者的生命周期,自动解决组件间的依赖关系,实现松散耦合
中间件 (Middleware) N/A (通过 MiddlewareConsumer) 在控制器处理请求之前执行的函数,用于身份验证、日志记录等。
守卫 (Guards) @Injectable(), @UseGuards() 在请求进入控制器方法之前执行,用于授权(判断用户是否有权访问此路由)。
拦截器 (Interceptors) @Injectable(), @UseInterceptors() 拦截请求响应,用于转换数据、添加缓存、或执行请求/响应日志记录等。
管道 (Pipes) @Injectable(), @UsePipes() 在控制器方法被调用之前执行,用于数据验证(Validation)和数据转换(Transformation)。
过滤器 (Exception Filters) @Catch(), @UseFilters() 捕获并处理应用中未处理的异常,定制化返回给客户端的错误响应格式。

nest从发送请求的响应的流程大致是这样的

总结来说,模块 是应用的骨架,控制器 是入口,提供者 是核心业务逻辑,而 守卫、管道、拦截器 等则是 NestJS 提供的强大工具,用于处理请求生命周期中的授权、验证和转换等任务。

以上就是nest的一些简单介绍,想详细了解的可以到nest的官网学习:

# NestJS 中文文档

# NestJS 英文文档

使用nest实现一个简单的URUD功能

首先nest有一个自己的cli可以让我们快速创建一个nest项目,因此我们需要先安装一下nest Cli

js 复制代码
npm i -g @nestjs/cli

安装完nest Cli后,就可以创建项目了

js 复制代码
nest new [项目名称]

// CLI 会询问你使用哪个包管理器(npm、yarn 或 pnpm),选择后会自动创建项目并安装依赖。

新建完项目后,可以看到我们的项目结构是这样的:

bash 复制代码
project-name/
├── src/
│   ├── main.ts              # 应用入口文件
│   ├── app.module.ts        # 根模块
│   ├── app.controller.ts    # 根控制器
│   └── app.service.ts       # 根服务
├── test/                    # 测试文件
├── dist/                    # 编译后的文件
├── node_modules/            # 依赖包
├── package.json             # 项目配置和依赖
├── tsconfig.json            # TypeScript 配置
├── nest-cli.json            # NestJS CLI 配置
└── README.md                # 项目说明

主要实现一个用户模块,在项目代码中有较为详细的注解说明。

以下是一个简单的User模块的代码,为了方便理解,先用一个示例来简单说它们的大概流程(创建用户):

1. 请求入口

http 复制代码
POST /api/users
Content-Type: application/json

{
  "name": "张三",
  "email": "zhangsan@example.com",
  "age": 25
}

发送一个POST类型的接口,地址/api/users,下方的请求体是请求体body。

2. 路由匹配

文件: user.controller.ts

js 复制代码
@Controller('api/users')  // 路由前缀
export class UserController {
  @Post()  // 匹配 POST /api/users
  async create(@Body() createUserDto: CreateUserDto) {
    return await this.userService.create(createUserDto);
  }
}

流程说明:

  1. NestJS 路由系统匹配 POST /api/users
  2. 找到 UserControllercreate 方法
  3. 准备执行方法

3. 数据验证(ValidationPipe)

文件:main.ts

typescript 复制代码
app.useGlobalPipes(
  new ValidationPipe({
    whitelist: true,              // 过滤未定义的属性
    forbidNonWhitelisted: true,  // 禁止未定义的属性
    transform: true,              // 自动转换类型
  }),
);

验证流程:

  1. @Body() 装饰器提取请求体 JSON 数据
  2. ValidationPipe 根据 CreateUserDto 的验证规则进行验证:
    • name: 必须是字符串(@IsString()
    • email: 必须是有效邮箱格式(@IsEmail()
    • age: 可选,如果提供必须是 0-150 的整数(@IsOptional(), @IsInt(), @Min(0), @Max(150)
  3. 如果验证失败,返回 400 Bad Request,包含错误详情
  4. 如果验证通过,将数据转换为 CreateUserDto 实例

验证规则(CreateUserDto):

typescript 复制代码
export class CreateUserDto {
  @IsString({ message: '用户名必须是字符串' })
  name: string;

  @IsEmail({}, { message: '邮箱格式不正确' })
  email: string;

  @IsOptional()
  @IsInt({ message: '年龄必须是整数' })
  @Min(0, { message: '年龄不能小于0' })
  @Max(150, { message: '年龄不能大于150' })
  age?: number;
}

4. 控制器处理

文件: user.controller.ts

typescript 复制代码
async create(@Body() createUserDto: CreateUserDto): Promise<UserResponseDto> {
  return await this.userService.create(createUserDto);
}

流程说明:

  1. 接收验证后的 CreateUserDto 对象
  2. 调用 userService.create() 方法
  3. 等待服务层返回结果
  4. 返回 UserResponseDto 类型的数据

5. 服务层处理

文件: user.service.ts

typescript 复制代码
async create(createUserDto: CreateUserDto): Promise<User> {
  // 1. 创建实体实例
  const newUser = this.userRepository.create({
    name: createUserDto.name,
    email: createUserDto.email,
    age: createUserDto.age,
  });

  // 2. 保存到数据库
  return await this.userRepository.save(newUser);
}

流程说明:

  1. 创建实体实例

    • repository.create() 创建 User 实体实例
    • 此时实体还未保存到数据库
    • 只设置了基本字段(name, email, age)
  2. 保存到数据库

    • repository.save() 执行 INSERT 操作
    • TypeORM 自动处理:
      • 生成自增 ID(@PrimaryGeneratedColumn()
      • 设置 createdAt@CreateDateColumn()
      • 设置 updatedAt@UpdateDateColumn()

主要涉及的文件main.ts,主要入口,用于项目的入口。

js 复制代码
import { NestFactory } from '@nestjs/core';
import { AppModule } from './app.module';
import { ValidationPipe } from '@nestjs/common';

/**
 * 应用程序启动入口
 * bootstrap 函数是 NestJS 应用的入口点
 */
async function bootstrap() {
  // 使用 NestFactory 创建 NestJS 应用实例
  // NestFactory 是创建应用的核心工厂类
  // AppModule 是应用的根模块,包含了所有功能模块的配置
  const app = await NestFactory.create(AppModule);
  
  /**
   * 启用跨域资源共享 (CORS)
   * 允许前端应用(运行在 localhost:3000)访问后端 API
   * credentials: true 表示允许携带认证信息(如 cookies)
   */
  app.enableCors({
    origin: 'http://localhost:3000', // 允许的前端域名
    credentials: true, // 允许携带凭证
  });

  /**
   * 全局验证管道
   * 自动验证所有传入的请求数据,根据 DTO 中的装饰器进行验证
   */
  app.useGlobalPipes(
    new ValidationPipe({
      whitelist: true, // 自动过滤掉 DTO 中未定义的属性
      forbidNonWhitelisted: true, // 如果请求包含未定义的属性,抛出错误
      transform: true, // 自动将请求数据转换为 DTO 实例
    }),
  );

  /**
   * 启动 HTTP 服务器
   * process.env.PORT 从环境变量读取端口,如果未设置则默认使用 3001
   * ?? 是空值合并运算符,当左侧为 null 或 undefined 时使用右侧的值
   */
  await app.listen(process.env.PORT ?? 3001);
  console.log(`Application is running on: http://localhost:${process.env.PORT ?? 3001}`);
}

// 执行启动函数
bootstrap();

app.module.ts,模块如果需要在当前模块使用那就需要进行导入。

js 复制代码
import { Module } from '@nestjs/common';
import { ConfigModule, ConfigService } from '@nestjs/config';
import { TypeOrmModule } from '@nestjs/typeorm';
import { AppController } from './app.controller';
import { AppService } from './app.service';
import { UserModule } from './user/user.module';

/**
 * AppModule - 应用的根模块
 * 
 * @Module 装饰器用于定义一个模块类
 * 模块是 NestJS 中组织代码的基本单元,类似于 Angular 的模块概念
 * 
 * 模块的三个主要属性:
 * - imports: 导入其他模块,使当前模块可以使用其他模块导出的功能
 * - controllers: 注册控制器,处理 HTTP 请求
 * - providers: 注册提供者(服务),实现业务逻辑,可以在整个模块中注入使用
 */
@Module({
  imports: [
    /**
     * ConfigModule - 配置模块
     * 
     * 作用:管理应用配置,支持从环境变量、配置文件等读取配置
     * 
     * 参数说明:
     *   - isGlobal: true - 设置为全局模块,所有模块都可以直接使用 ConfigService
     *   - envFilePath: '.env' - 环境变量文件路径
     *   - load: [] - 可以加载额外的配置源
     * 
     * 说明:
     *   - 全局模块意味着不需要在每个模块中导入 ConfigModule
     *   - 可以直接在任何地方注入 ConfigService 使用
     */
    ConfigModule.forRoot({
      isGlobal: true, // 全局配置模块
      envFilePath: '.env', // 环境变量文件路径
    }),

    /**
     * TypeOrmModule.forRootAsync() - TypeORM 异步配置
     * 
     * 作用:配置 TypeORM 数据库连接
     * 
     * 为什么使用 forRootAsync:
     *   - 可以异步获取配置(如从 ConfigService)
     *   - 支持依赖注入(如注入 ConfigService)
     *   - 更灵活,可以在运行时读取配置
     * 
     * 参数说明:
     *   - imports: [ConfigModule] - 导入 ConfigModule 以使用 ConfigService
     *   - useFactory: 工厂函数,返回 TypeORM 配置对象
     *   - inject: [ConfigService] - 注入 ConfigService 到工厂函数
     */
    TypeOrmModule.forRootAsync({
      imports: [ConfigModule],
      useFactory: (configService: ConfigService) => ({
        /**
         * type - 数据库类型
         * 支持:mysql, postgres, sqlite, mssql, mongodb 等
         */
        type: 'mysql',

        /**
         * host - 数据库主机地址
         * 从环境变量 DB_HOST 读取,默认 localhost
         */
        host: configService.get('DB_HOST', 'localhost'),

        /**
         * port - 数据库端口
         * 从环境变量 DB_PORT 读取,默认 3306(MySQL 默认端口)
         */
        port: configService.get<number>('DB_PORT', 3306),

        /**
         * username - 数据库用户名
         * 从环境变量 DB_USERNAME 读取
         */
        username: configService.get('DB_USERNAME', 'root'),

        /**
         * password - 数据库密码
         * 从环境变量 DB_PASSWORD 读取
         */
        password: configService.get('DB_PASSWORD', ''),

        /**
         * database - 数据库名称
         * 从环境变量 DB_DATABASE 读取
         */
        database: configService.get('DB_DATABASE', 'nest_db'),

        /**
         * entities - 实体类数组
         * 指定 TypeORM 要管理的实体类

         */
        entities: [__dirname + '/**/*.entity{.ts,.js}'],

        /**
         * synchronize - 自动同步数据库结构
         * 
         * 作用:根据实体类自动创建或更新数据库表结构
         * 
         * 注意:
         *   - true: 开发环境使用,自动同步表结构(方便开发)
         *   - false: 生产环境必须设为 false,使用迁移(migration)管理数据库结构
         *   - 设为 true 时,修改实体类会自动修改数据库表,可能丢失数据
         */
        synchronize: configService.get('DB_SYNCHRONIZE', 'true') === 'true',

        /**
         * logging - SQL 日志
         * 
         * 作用:是否打印执行的 SQL 语句
         * 
         * 说明:
         *   - true: 开发环境使用,方便调试,可以看到执行的 SQL
         *   - false: 生产环境使用,减少日志输出
         *   - 也可以设置为 ['query', 'error'] 等数组,只记录特定类型的日志
         */
        logging: configService.get('DB_LOGGING', 'true') === 'true',

        /**
         * 其他常用配置选项:
         * 
         * - charset: 'utf8mb4' - 字符集(支持 emoji)
         * - timezone: '+08:00' - 时区
         * - extra: { connectionLimit: 10 } - 连接池配置
         * - migrations: [] - 数据库迁移文件
         * - subscribers: [] - 订阅者(实体监听器)
         */
      }),
      inject: [ConfigService],
    }),
    UserModule, // 导入用户模块,这样才可以使用user模块中导出的方法
  ],
  controllers: [AppController], // 注册应用控制器
  providers: [AppService], // 注册应用服务
})
export class AppModule {}

user.controller.ts,在user模块中的控制器,用于路由的匹配。

js 复制代码
import {
  Controller,Get,Post,Body,Patch,Param,Delete,ParseIntPipe,} from '@nestjs/common';
import { UserService } from './user.service';
import { CreateUserDto } from './dto/create-user.dto';
import { UpdateUserDto } from './dto/update-user.dto';
import { UserResponseDto } from './dto/user-response.dto';

/**
 * UserController - 用户控制器
 * 
 * 负责处理用户相关的 HTTP 请求
 * 实现完整的 CRUD 操作:Create(创建)、Read(读取)、Update(更新)、Delete(删除)
 */

/**
 * @Controller('api/users') - 控制器路由装饰器
 * 
 * 作用:定义控制器的路由前缀,所有该控制器下的路由都会自动加上这个前缀
 * 参数:'api/users' - 路由前缀路径
 * 效果:
 *   - 如果方法装饰器是 @Get(),完整路径为 GET /api/users
 *   - 如果方法装饰器是 @Get(':id'),完整路径为 GET /api/users/:id
 */
 
@Controller('api/users')
export class UserController {
  /**
   * 依赖注入 - 通过构造函数注入 UserService
   * private readonly 表示这是私有只读属性,只能在类内部使用
   */
  constructor(private readonly userService: UserService) {}
  
  /**
   * 创建用户
   * POST /api/users
   */
  /**
   * @Post() - HTTP POST 方法装饰器
   * 
   * 作用:将方法标记为处理 POST 请求的路由处理器
   * 参数:不传参数时,路由路径为空,完整路径为控制器的前缀 + 空路径 = /api/users
   * 说明:POST 请求通常用于创建新资源
   */
  @Post()
  async create(
    /**
     * @Body() - 请求体参数装饰器
     * 
     * 作用:从 HTTP 请求体中提取数据并注入到方法参数中
     * 说明:
     *   - NestJS 会自动将 JSON 请求体解析为 CreateUserDto 对象
     *   - 结合 ValidationPipe,会自动验证数据是否符合 DTO 中定义的规则
     *   - 如果验证失败,会返回 400 错误
     */
    @Body() createUserDto: CreateUserDto
  ): Promise<UserResponseDto> {
    return await this.userService.create(createUserDto);
  }

  /**
   * 获取所有用户
   * GET /api/users
   */

  /**
   * @Get() - HTTP GET 方法装饰器
   * 
   * 作用:将方法标记为处理 GET 请求的路由处理器
   * 参数:不传参数时,路由路径为空,完整路径为 /api/users
   * 说明:GET 请求用于获取资源
   */
  @Get()
  async findAll(): Promise<UserResponseDto[]> {
    return await this.userService.findAll();
  }

  /**
   * 根据ID获取单个用户
   * GET /api/users/:id
   */

  /**
   * @Get(':id') - HTTP GET 方法装饰器(带路径参数)
   * 
   * 作用:定义带路径参数的 GET 路由
   * 参数:':id' - 路径参数,冒号表示这是一个动态参数
   * 完整路径:GET /api/users/:id
   * 说明:路径参数可以通过 @Param() 装饰器获取
   */
  @Get(':id')
  async findOne(
    /**
     * @Param('id', ParseIntPipe) - 路径参数装饰器 + 管道转换
     * 
     * 作用:
     *   1. @Param('id') - 从路由路径中提取名为 'id' 的参数
     *   2. ParseIntPipe - 将字符串参数转换为整数类型
     * 
     * 说明:
     *   - HTTP 请求中的路径参数默认是字符串类型
     *   - ParseIntPipe 会自动将字符串转换为数字
     *   - 如果转换失败(如传入非数字),会自动返回 400 错误
     *   - 这是 NestJS 中类型转换和验证的常用方式
     */
    @Param('id', ParseIntPipe) id: number
  ): Promise<UserResponseDto> {
    return await this.userService.findOne(id);
  }

  /**
   * 更新用户信息
   * PATCH /api/users/:id
   */

  /**
   * @Patch(':id') - HTTP PATCH 方法装饰器
   * 
   * 作用:将方法标记为处理 PATCH 请求的路由处理器
   * 参数:':id' - 路径参数,表示要更新的资源ID
   * 说明:
   *   - PATCH 用于部分更新资源(只更新提供的字段)
   *   - 与 PUT 的区别:PUT 通常用于完整替换资源,PATCH 用于部分更新
   *   - RESTful API 中,更新操作通常使用 PATCH
   */
  @Patch(':id')
  async update(
    /**
     * @Param('id', ParseIntPipe) - 提取并转换路径参数 id
     */
    @Param('id', ParseIntPipe) id: number,
    /**
     * @Body() - 提取请求体数据
     * 
     * 说明:UpdateUserDto 中的字段都是可选的,可以只更新部分字段
     */
    @Body() updateUserDto: UpdateUserDto,
  ): Promise<UserResponseDto> {
    return await this.userService.update(id, updateUserDto);
  }

  /**
   * 删除用户
   * DELETE /api/users/:id
   */

  /**
   * @Delete(':id') - HTTP DELETE 方法装饰器
   * 
   * 作用:将方法标记为处理 DELETE 请求的路由处理器
   * 参数:':id' - 路径参数,表示要删除的资源ID
   * 说明:DELETE 请求用于删除资源,是 RESTful API 中删除操作的标准方法
   */
  @Delete(':id')
  async remove(
    /**
     * @Param('id', ParseIntPipe) - 提取并转换路径参数 id
     */
    @Param('id', ParseIntPipe) id: number
  ): Promise<void> {
    return await this.userService.remove(id);
  }
}

user.service.ts,在user模块中的提供者,用于处理请求的业务逻辑。

js 复制代码
import { Injectable, NotFoundException } from '@nestjs/common';
import { InjectRepository } from '@nestjs/typeorm';
import { Repository } from 'typeorm';
import { User } from './entities/user.entity';
import { CreateUserDto } from './dto/create-user.dto';
import { UpdateUserDto } from './dto/update-user.dto';

/**
 * UserService - 用户服务类
 * 
 * 负责处理用户相关的业务逻辑
 * 使用 TypeORM Repository 进行数据库操作
 */

/**
 * @Injectable() - 可注入装饰器
 * 
 * 作用:标记此类可以被 NestJS 的依赖注入系统管理
 * 
 * 功能说明:
 *   1. 使此类可以被其他类(如控制器)通过构造函数注入使用
 *   2. NestJS 会自动管理其生命周期(默认是单例模式,整个应用只有一个实例)
 *   3. 可以在模块的 providers 数组中注册
 *   4. 支持依赖注入,可以在构造函数中注入其他服务
 * 
 * 生命周期:
 *   - 默认作用域:单例(SINGLETON),应用启动时创建一次,所有地方共享同一个实例
 *   - 可以通过 { scope: Scope.REQUEST } 设置为请求作用域,每个请求创建新实例
 *   - 可以通过 { scope: Scope.TRANSIENT } 设置为瞬态作用域,每次注入都创建新实例
 * 
 * 使用场景:
 *   - 封装业务逻辑
 *   - 数据访问层(如数据库操作)
 *   - 可复用的功能模块
 */
@Injectable()
export class UserService {
  /**
   * 构造函数 - 依赖注入 Repository
   * 
   * @InjectRepository(User) - Repository 注入装饰器
   * 
   * 作用:注入 TypeORM Repository,用于数据库操作
   * 
   * 参数说明:
   *   - User - 实体类,指定要操作的实体类型
   * 
   * 说明:
   *   - Repository<User> 是 TypeORM 提供的数据库操作接口
   *   - 提供了丰富的数据库操作方法(find, save, delete, update 等)
   *   - 类型安全:TypeScript 会根据 User 实体类提供类型提示
   *   - 需要在 UserModule 中使用 TypeOrmModule.forFeature([User]) 注册
   */
  constructor(
    @InjectRepository(User)
    private userRepository: Repository<User>,
  ) {}

  /**
   * 创建用户
   * 
   * @param createUserDto 创建用户的数据
   * @returns 创建的用户对象(Promise)
   * 
   * 说明:
   *   - create() 方法创建实体实例,但不保存到数据库
   *   - save() 方法将实体保存到数据库
   *   - TypeORM 会自动处理 createdAt 和 updatedAt 字段
   */
  async create(createUserDto: CreateUserDto): Promise<User> {
    // 创建实体实例
    const newUser = this.userRepository.create({
      name: createUserDto.name,
      email: createUserDto.email,
      age: createUserDto.age,
    });

    // 保存到数据库
    // save() 方法会:
    // 1. 插入新记录到数据库
    // 2. 自动设置 createdAt 和 updatedAt(因为使用了 @CreateDateColumn 和 @UpdateDateColumn)
    // 3. 返回保存后的实体(包含自动生成的 id)
    return await this.userRepository.save(newUser);
  }

  /**
   * 获取所有用户
   * 
   * @returns 用户数组(Promise)
   * 
   * 说明:
   *   - find() 方法查询所有记录
   *   - 返回数组,如果没有记录则返回空数组 []
   */
  async findAll(): Promise<User[]> {
    return await this.userRepository.find();
  }

  /**
   * 根据ID获取单个用户
   * 
   * @param id 用户ID
   * @returns 用户对象(Promise)
   * @throws NotFoundException 如果用户不存在
   * 
   * 说明:
   *   - findOne({ where: { id } }) 根据条件查询单条记录
   *   - 如果找不到记录,返回 null
   *   - 需要手动检查并抛出异常
   */
  async findOne(id: number): Promise<User> {
    const user = await this.userRepository.findOne({ where: { id } });

    if (!user) {
      throw new NotFoundException(`用户 ID ${id} 不存在`);
    }

    return user;
  }

  /**
   * 更新用户信息
   * 
   * @param id 用户ID
   * @param updateUserDto 更新的数据
   * @returns 更新后的用户对象(Promise)
   * @throws NotFoundException 如果用户不存在
   * 
   * 说明:
   *   - 先查找用户,如果不存在则抛出异常
   *   - 使用 Object.assign() 或展开运算符更新字段
   *   - save() 方法会:
   *     1. 如果实体有 id,执行 UPDATE 操作
   *     2. 自动更新 updatedAt 字段(因为使用了 @UpdateDateColumn)
   *     3. 返回更新后的实体
   */
  async update(id: number, updateUserDto: UpdateUserDto): Promise<User> {
    // 查找用户,如果不存在会抛出异常
    const user = await this.findOne(id);

    // 更新用户信息
    // Object.assign() 将 updateUserDto 中的属性复制到 user 对象
    // 只更新提供的字段,未提供的字段保持不变
    Object.assign(user, updateUserDto);

    // 保存更新
    // save() 方法检测到实体有 id,会执行 UPDATE 而不是 INSERT
    // @UpdateDateColumn 装饰的字段会自动更新为当前时间
    return await this.userRepository.save(user);
  }

  /**
   * 删除用户
   * 
   * @param id 用户ID
   * @throws NotFoundException 如果用户不存在
   * 
   * 说明:
   *   - remove() 方法需要传入实体对象
   *   - delete() 方法可以直接传入 id,更高效
   *   - 两种方式都可以,delete() 更简单直接
   */
  async remove(id: number): Promise<void> {
    // 先检查用户是否存在
    await this.findOne(id); // 如果不存在会抛出异常

    // 删除用户
    // delete() 方法根据 id 删除记录
    // 返回 DeleteResult 对象,包含 affected 属性(受影响的行数)
    await this.userRepository.delete(id);
  }
}

以上涉及的DTO有三个,代码如下:

create-user.dto.ts:

js 复制代码
import { IsString, IsEmail, IsOptional, IsInt, Min, Max } from 'class-validator';

/**
 * CreateUserDto - 创建用户数据传输对象
 * 用于接收创建用户的请求数据
 * 
 * DTO (Data Transfer Object) 的作用:
 *   1. 定义 API 接口的请求数据结构
 *   2. 提供数据验证规则(通过 class-validator 装饰器)
 *   3. 类型安全:TypeScript 类型检查
 */
export class CreateUserDto {
  /**
   * @IsString() - 字符串验证装饰器
   * 
   * 作用:验证该字段必须是字符串类型
   * 
   * 参数说明:
   *   - message: 验证失败时返回的错误消息
   * 
   * 验证规则:
   *   - 如果传入的值不是字符串,验证会失败
   *   - 验证失败时,NestJS 会返回 400 错误,错误信息为 "用户名必须是字符串"
   * 
   * 说明:
   *   - class-validator 提供了丰富的验证装饰器
   *   - 需要配合 ValidationPipe 使用(在 main.ts 中已配置)
   *   - 验证在请求到达控制器方法之前自动执行
   */
  @IsString({ message: '用户名必须是字符串' })
  name: string;
  
  /**
   * @IsEmail() - 邮箱格式验证装饰器
   * 
   * 作用:验证该字段必须是有效的邮箱格式
   * 
   * 参数说明:
   *   - 第一个参数 {}: 邮箱验证选项(使用默认选项)
   *   - 第二个参数 { message: ... }: 验证失败时的错误消息
   * 
   * 验证规则:
   *   - 必须符合邮箱格式(如:user@example.com)
   *   - 验证失败时返回 "邮箱格式不正确"
   * 
   * 说明:
   *   - 这是数据验证的重要环节,防止无效数据进入系统
   *   - 验证在服务层处理之前完成,提高系统安全性
   */
  @IsEmail({}, { message: '邮箱格式不正确' })
  email: string;

  /**
   * @IsOptional() - 可选字段装饰器
   * 
   * 作用:标记该字段是可选的,如果未提供值,跳过后续验证
   * 
   * 说明:
   *   - 如果字段存在,则执行后续的验证规则
   *   - 如果字段不存在(undefined),则跳过验证
   *   - 必须放在其他验证装饰器之前
   *   - 与 TypeScript 的可选属性(?)配合使用
   */
  @IsOptional()

  /**
   * @IsInt() - 整数验证装饰器
   * 
   * 作用:验证该字段必须是整数
   * 
   * 说明:
   *   - 验证值是否为整数类型
   *   - 如果传入小数或非数字,验证会失败
   */
  @IsInt({ message: '年龄必须是整数' })

  /**
   * @Min() - 最小值验证装饰器
   * 
   * 作用:验证数值必须大于或等于指定值
   * 
   * 参数:0 - 最小值
   * 说明:年龄不能为负数
   */
  @Min(0, { message: '年龄不能小于0' })

  /**
   * @Max() - 最大值验证装饰器
   * 
   * 作用:验证数值必须小于或等于指定值
   * 
   * 参数:200 - 最大值
   * 说明:年龄不能超过 200
   * 
   * 验证链说明:
   *   1. @IsOptional() - 如果字段不存在,跳过后续验证
   *   2. @IsInt() - 如果字段存在,必须是整数
   *   3. @Min(0) - 如果字段存在且是整数,必须 >= 0
   *   4. @Max(150) - 如果字段存在且是整数,必须 <= 150
   * 
   * 验证顺序:从上到下依次执行,任何一个验证失败都会返回错误
   */
  @Max(200, { message: '年龄不能大于200' })
  age?: number;
}

update-user.dto.ts:

js 复制代码
import { IsString, IsEmail, IsOptional, IsInt, Min, Max } from 'class-validator';

/**
 * UpdateUserDto - 更新用户数据传输对象
 * 用于接收更新用户的请求数据
 * 所有字段都是可选的,因为更新时可能只更新部分字段
 * 
 * 与 CreateUserDto 的区别:
 *   - CreateUserDto: 所有必填字段都不能为空(name、email 必填)
 *   - UpdateUserDto: 所有字段都是可选的,符合 PATCH 部分更新的语义
 * 
 * 设计原则:
 *   - 部分更新:用户可以只更新需要修改的字段
 *   - 灵活性:不需要提供所有字段,只提供要更新的字段即可
 *   - 验证:如果提供了字段,则必须符合验证规则
 */
export class UpdateUserDto {

  /**
   * @IsOptional() - 标记为可选字段
   * 
   * 说明:更新操作中,如果客户端不提供此字段,表示不更新该字段
   */
  @IsOptional()

  /**
   * @IsString() - 如果提供了 name 字段,则必须是字符串
   * 
   * 验证逻辑:
   *   - 如果 name 未提供(undefined):跳过验证(因为 @IsOptional)
   *   - 如果 name 提供了:必须是字符串类型
   */
  @IsString({ message: '用户名必须是字符串' })
  name?: string;

  /**
   * @IsOptional() - 标记为可选
   */
  @IsOptional()

  /**
   * @IsEmail() - 如果提供了 email 字段,则必须是有效邮箱格式
   * 
   * 说明:更新邮箱时,必须提供有效的邮箱格式
   */
  @IsEmail({}, { message: '邮箱格式不正确' })
  email?: string;

  /**
   * @IsOptional() - 标记为可选
   */
  @IsOptional()

  /**
   * @IsInt() - 如果提供了 age 字段,则必须是整数
   */
  @IsInt({ message: '年龄必须是整数' })

  /**
   * @Min(0) - 如果提供了 age 字段,则必须 >= 0
   */
  @Min(0, { message: '年龄不能小于0' })

  /**
   * @Max(200) - 如果提供了 age 字段,则必须 <= 200
   * 
   * 验证链说明(与 CreateUserDto 相同):
   *   1. @IsOptional() - 字段可选
   *   2. 如果提供了值,则必须通过后续所有验证
   *   3. 验证顺序:IsInt -> Min -> Max
   */
  @Max(200, { message: '年龄不能大于200' })
  age?: number;
}

user-response.dto.ts :

js 复制代码
/**
 * UserResponseDto - 用户响应数据传输对象
 * 用于 API 响应,包含完整的用户信息
 * 
 * 作用:
 *   1. 定义 API 响应的数据结构
 *   2. 类型安全:确保响应数据的类型正确
 * 
 * 与 User 实体的区别:
 *   - User 实体:内部数据结构,可能包含敏感信息或内部字段
 *   - UserResponseDto:对外暴露的数据结构,只包含需要返回给客户端的字段
 * 
 * 最佳实践:
 *   - 使用 DTO 控制返回给客户端的数据
 *   - 可以隐藏敏感信息(如密码)
 *   - 可以格式化数据(如日期格式)
 *   - 可以添加额外的计算字段
 */
export class UserResponseDto {
  id: number;
  
  name: string;

  email: string;

  age?: number;

  createdAt: Date;

  updatedAt: Date;
}

user.entity.ts是用户实体类(数据库表映射)。

js 复制代码
import {
  Entity,
  Column,
  PrimaryGeneratedColumn,
  CreateDateColumn,
  UpdateDateColumn,
} from 'typeorm';

/**
 * User 实体类 - TypeORM 实体
 * 
 * 作用:
 *   1. 定义数据库表结构
 *   2. 映射数据库表到 TypeScript 类
 *   3. 提供类型安全的数据库操作
 * 
 * @Entity('users') - 实体装饰器
 * 
 * 作用:标记这是一个 TypeORM 实体类
 * 参数:'users' - 数据库表名(如果不指定,默认使用类名的小写形式)
 * 
 * 说明:
 *   - TypeORM 会根据这个实体类自动创建或同步数据库表
 *   - 实体类中的属性会映射到数据库表的列
 */
@Entity('users')
export class User {
  /**
   * @PrimaryGeneratedColumn() - 主键自增列装饰器
   * 
   * 作用:定义主键列,自动生成递增的 ID
   * 
   * 说明:
   *   - 这是数据库表的主键
   *   - 自动递增,每次插入新记录时自动生成
   *   - 类型为 number,对应 MySQL 的 INT 或 BIGINT
   */
  @PrimaryGeneratedColumn()
  id: number;

  /**
   * @Column() - 普通列装饰器
   * 
   * 作用:定义数据库表的普通列
   * 
   * 参数说明:
   *   - type: 'varchar' - 数据库列类型,varchar 是可变长度字符串
   *   - length: 255 - 最大长度
   *   - nullable: false - 不允许为空(必填字段)
   * 
   * 说明:
   *   - 如果不指定 type,TypeORM 会根据 TypeScript 类型推断
   *   - string 类型默认映射为 varchar(255)
   */
  @Column({ type: 'varchar', length: 255, nullable: false })
  name: string;

  /**
   * @Column() - 邮箱列
   * 
   * 参数说明:
   *   - unique: true - 唯一约束,确保邮箱不重复
   *   - nullable: false - 不允许为空
   * 
   * 说明:
   *   - unique: true 会在数据库层面创建唯一索引
   *   - 如果尝试插入重复邮箱,数据库会抛出错误
   */
  @Column({ type: 'varchar', length: 255, unique: true, nullable: false })
  email: string;

  /**
   * @Column() - 年龄列(可选字段)
   * 
   * 参数说明:
   *   - type: 'int' - 整数类型
   *   - nullable: true - 允许为空(可选字段)
   *   - default: null - 默认值为 null
   * 
   * 说明:
   *   - 可选字段在 TypeScript 中使用 ? 标记
   *   - 数据库列也设置为 nullable: true
   */
  @Column({ type: 'int', nullable: true, default: null })
  age?: number;

  /**
   * @CreateDateColumn() - 创建时间列装饰器
   * 
   * 作用:自动管理记录的创建时间
   * 
   * 说明:
   *   - TypeORM 会在插入新记录时自动设置当前时间
   *   - 类型为 Date,对应 MySQL 的 DATETIME 或 TIMESTAMP
   *   - 不需要手动设置,TypeORM 会自动处理
   */
  @CreateDateColumn({ type: 'timestamp' })
  createdAt: Date;

  /**
   * @UpdateDateColumn() - 更新时间列装饰器
   * 
   * 作用:自动管理记录的更新时间
   * 
   * 说明:
   *   - TypeORM 会在更新记录时自动更新为当前时间
   *   - 每次执行 UPDATE 操作时,此字段会自动更新
   *   - 不需要手动设置,TypeORM 会自动处理
   */
  @UpdateDateColumn({ type: 'timestamp' })
  updatedAt: Date;
}

user.module.ts,user的模块,导入其他模块的提供者,或者导出自己的提供者给其他的模块导入。

js 复制代码
import { Module } from '@nestjs/common';
import { TypeOrmModule } from '@nestjs/typeorm';
import { UserService } from './user.service';
import { UserController } from './user.controller';
import { User } from './entities/user.entity';

/**
 * UserModule - 用户模块
 * 
 * 封装用户相关的所有功能:
 * - UserController: 处理用户相关的 HTTP 请求
 * - UserService: 处理用户相关的业务逻辑
 * - User Entity: 用户实体类(数据库表映射)
 */

/**
 * @Module() - 模块装饰器
 * 
 * 作用:定义一个 NestJS 模块,是组织代码的基本单元
 * 
 * 参数说明:
 *   - controllers: 注册该模块中的控制器
 *     * 作用:使控制器可以处理 HTTP 请求
 *     * 说明:只有在此数组中注册的控制器才能被 NestJS 识别和处理
 * 
 *   - providers: 注册该模块中的提供者(服务、工厂等)
 *     * 作用:使服务可以被依赖注入系统管理
 *     * 说明:只有在此数组中注册的服务才能被注入使用
 *     * 注意:服务默认是单例的,整个应用共享一个实例
 * 
 *   - exports: 导出该模块中的提供者
 *     * 作用:使其他模块可以导入并使用这些提供者
 *     * 说明:
 *       - 只有导出的服务才能被其他模块使用
 *       - 如果 UserService 被导出,其他模块导入 UserModule 后就可以使用 UserService
 *       - 这是模块间共享功能的标准方式
 * 
 *   - imports: 导入其他模块
 *     * 作用:使当前模块可以使用其他模块导出的功能
 *     * 说明:例如导入 TypeOrmModule.forFeature() 可以使用数据库 Repository
 * 
 * 模块的作用:
 *   1. 代码组织:将相关功能组织在一起
 *   2. 依赖管理:管理模块内部的依赖关系
 *   3. 封装:隐藏内部实现,只暴露必要的接口
 *   4. 可复用:导出的服务可以在多个模块中复用
 * 
 * 模块的层次结构:
 *   - 根模块(AppModule):应用的入口模块
 *   - 功能模块(如 UserModule):封装特定功能的模块
 *   - 共享模块:提供通用功能的模块
 */
@Module({
  /**
   * TypeOrmModule.forFeature([User]) - 注册实体到 TypeORM
   * 
   * 作用:在当前模块中注册 TypeORM 实体,使该模块可以使用 Repository
   * 
   * 参数说明:
   *   - [User] - 实体类数组,指定要在该模块中使用的实体
   * 
   * 说明:
   *   - forFeature() 必须在每个需要使用 Repository 的模块中调用
   *   - 注册后,可以在该模块的服务中注入 Repository<User>
   *   - 与 forRoot() 的区别:
   *     * forRoot(): 在根模块中配置数据库连接(全局配置)
   *     * forFeature(): 在功能模块中注册实体(模块级配置)
   * 
   * 使用示例:
   *   // user.service.ts
   *   constructor(
   *     @InjectRepository(User)
   *     private userRepository: Repository<User>,
   *   ) {}
   */
  imports: [TypeOrmModule.forFeature([User])],

  /**
   * controllers - 控制器数组
   * 
   * 作用:注册该模块中的控制器
   * 说明:UserController 会被注册,可以处理 /api/users 相关的请求
   */
  controllers: [UserController],

  /**
   * providers - 提供者数组
   * 
   * 作用:注册该模块中的服务
   * 说明:UserService 会被注册,可以在 UserController 中注入使用
   */
  providers: [UserService],

  /**
   * exports - 导出数组
   * 
   * 作用:导出 UserService,使其他模块可以导入 UserModule 并使用 UserService
   * 使用场景:
   *   - 如果其他模块(如 OrderModule)需要使用 UserService
   *   - 可以在 OrderModule 中导入 UserModule
   *   - 然后在 OrderService 中注入 UserService
   * 
   * 示例:
   *   // order.module.ts
   *   @Module({
   *     imports: [UserModule], // 导入 UserModule
   *     ...
   *   })
   *   
   *   // order.service.ts
   *   constructor(private userService: UserService) {} // 可以注入使用
   */
  exports: [UserService],
})
export class UserModule {}

总结

以上是 Nest.js 的基础介绍及简易 CRUD 实现,这是我入门 Nest.js 的第一步,也是个人学习记录。

想要深入学习,可优先查阅官方文档,也可参考其他博主的文章与视频。

不必纠结 "学 Nest.js 为何不直接学 Java/Go":作为前端开发者,我学习 Nest.js 的成本更低,且现阶段并非为了工作业务应用,更多是方便自己偶尔编写一些个人小Demo。

相关推荐
soda_yo2 小时前
React哲学:保持组件纯粹 哈气就要哈得纯粹
前端·react.js·设计
Bigger2 小时前
Tauri (22)——让 `Esc` 快捷键一层层退出的分层关闭方案
前端·react.js·app
大猫会长2 小时前
react中用css加载背景图的2种情况
开发语言·前端·javascript
编程修仙2 小时前
第一篇 VUE3的介绍以及搭建自己的VUE项目
前端·javascript·vue.js
search72 小时前
前端学习13:存储器
前端·学习
星月心城2 小时前
八股文-JavaScript(第一天)
开发语言·前端·javascript
政采云技术2 小时前
深入理解 Webpack5:从打包到热更新原理
前端·webpack
T___T2 小时前
从入门到实践:React Hooks 之 useState 与 useEffect 核心解析
前端·react.js·面试
山有木兮木有枝_2 小时前
当你的leader问你0.1+0.2=?
前端