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。

相关推荐
掘金者阿豪30 分钟前
把业务数据变成共享仪表盘:Metabase可视化与远程访问实践
前端·后端
kyriewen1 小时前
折腾了半年 AI 编程工作流,最后发现效率瓶颈是桌上那块屏幕
前端·javascript·ai编程
蜗牛前端1 小时前
codex 全流程开发上线的高颜值礼簿小程序
前端·微信小程序
大龄秃头程序员2 小时前
我在图文流 App 里落地双层缓存、弱网降级与 OOM 治理
前端
老王以为2 小时前
React Renderer 分离的多平台架构
前端·react native·react.js
hunterandroid2 小时前
Kotlin Coroutines 与 Flow:让异步任务更清晰
前端
Bigger3 小时前
从零搭建 AI 代码审查服务:一份前端也能看懂的 Python 学习笔记
前端·ci/cd·ai编程
lichenyang4533 小时前
JSAPI、NAPI、Biz、Imp:ASCF Demo 如何真正调用系统能力和 C++ 能力
前端
lichenyang4533 小时前
IPC、JSVM、UIThread、libuv:ASCF 架构图里最容易混的几个词
前端
用户059540174463 小时前
Redis记忆存储故障恢复测试踩坑实录:手动测试让我漏掉了2个一致性Bug
前端·css