NestJS全栈实战笔记:优雅处理 Entity 与 DTO 的映射与字段过滤

NestJS全栈实战笔记:优雅处理 Entity 与 DTO 的映射与字段过滤

在 NestJS + TypeORM 开发中,我发现经常面临一个两难困境:数据库实体 (Entity) 包含所有字段(密码、角色、审计时间),但 API 接口往往只需要部分字段。

如果直接返回 Entity,会泄露敏感信息;如果手动写无数个 DTO,又会产生大量重复代码,在Service层繁复的手动脱敏又不够优雅,本文记录了如何利用 Mapped Types序列化 (Serialization) 优雅解决这一问题,希望可以帮助到正在使用Nestjs开发全栈项目且对代码质量和可维护性有要求的开发者。

问题缘起

在项目开发过程中,我定义了一个包含 passworduserRoleisActive 等敏感字段的 User 实体。 但在编写 RESTful API 时,出现了以下痛点:

  1. 数据冗余与泄露:普通用户查询接口不应返回 userRolepassword,而管理员接口又需要这些信息。
  2. 类型不匹配:我尝试用 OmitType 派生一个"阉割版"Entity 在 Service 层流转,导致 Service 内部逻辑(如鉴权)无法获取到被切除的字段。
  3. 手动脱敏繁琐:在 Controller 或 Service 中手动写 user.password = '' 既不优雅也容易遗漏,而且心智负担极其昂贵,不符合DRY 原则(Don't Repeat Yourself)。

这个问题的核心矛盾在于,Service层需要"全量数据"处理业务,但Controller层需要"按需数据"响应客户端。

方案搜索与整合

为了优雅的解决这个问题,我打算从DTO和ORM映射两方面入手,利用AI工具查找和梳理了方案。

Nest.js的映射类型

NestJS 中文文档|映射类型

映射类型是Nestjs内部实现的一套工具函数,实现了类型转换,使这项任务更加便捷。包含Paticial,Pick,Omit等。

1 PartialType (部分类型)

场景 :用于更新操作(Patch),将基类所有属性变为可选

typescript 复制代码
import { PartialType } from '@nestjs/swagger';
import { CreateCatDto } from './create-cat.dto';

// UpdateCatDto 拥有 CreateCatDto 的所有属性,但都是可选的
export class UpdateCatDto extends PartialType(CreateCatDto) {}
2 PickType (挑选类型)

场景:只需要基类中的某几个字段。

typescript 复制代码
import { PickType } from '@nestjs/swagger';
import { CreateCatDto } from './create-cat.dto';

// 只包含 'name' 和 'age' 字段
export class CatAgeDto extends PickType(CreateCatDto, ['name', 'age'] as const) {}
3 OmitType (忽略类型)

场景:排除基类中的敏感字段或不需要的字段。

typescript 复制代码
import { OmitType } from '@nestjs/swagger';
import { CreateUserDto } from './create-user.dto';

// 排除 'password' 字段,保留其他所有字段
export class UserResponseDto extends OmitType(CreateUserDto, ['password'] as const) {}
4 IntersectionType (交叉类型)

场景:合并两个不同的 DTO。

typescript 复制代码
import { IntersectionType } from '@nestjs/swagger';

// 合并 CreateCatDto 和 AdditionalCatInfo 的属性
export class CombinedCatDto extends IntersectionType(
  CreateCatDto,
  AdditionalCatInfo,
) {}
5. 类型组合 (Composition)

这些映射类型可以相互嵌套组合,以构建更复杂的 DTO 逻辑。

示例 :创建一个用于更新的 DTO,但排除掉 id 字段,且其余字段可选。

typescript 复制代码
import { PartialType, OmitType } from '@nestjs/swagger';

// 1. 先排除 'id'
// 2. 再将剩余属性变为可选
export class UpdateCatDto extends PartialType(
  OmitType(CreateCatDto, ['id'] as const),
) {}

class-transformer @Exclude

class-transformer|@Exclude

可以利用class-transformer的装饰器来实现跳过特定字段

有时候你想在转换过程中跳过一些字段. 可以使用 @Exclude 装饰器:

ts 复制代码
import { Exclude } from 'class-transformer';

export class User {
  id: number;

  email: string;

  @Exclude()
  password: string;
}

现在当你转换User时, password 字段就会被跳过并不再出现在转换后的结果里。

敲定方案

解决该问题的核心在于 关注点分离 (Separation of Concerns)单一数据源 (Single Source of Truth)

  • Entity 不变 :Entity 类应完整映射数据库结构,不应为了迁就某个API接口而修改Entity的定义。
  • Mapped Types 减少样板代码 :利用 Swagger 的映射类型,我们可以基于 Entity自动生成 DTO,当 Entity 字段变更(如修改 Swagger 文档描述)时,DTO 自动同步。
  • DTO 决定响应结构:DTO (Data Transfer Object) 负责定义"给前端看什么"。
  • 序列化负责清洗数据 :TypeScript 的类型检查是编译时的,运行时的对象往往包含多余属性。必须使用序列化工具(如 class-transformer)在运行时剔除 DTO 中未定义的字段。

解决步骤与代码实现

1. 定义全量 Entity (配合 class-transformer)

TypeORM中的Entity是直接映射到数据库的表的,因此我们必须始终保持Entity的完整性。

对于那些全局敏感 字段(如密码),可以在 Entity 层面使用 @Exclude 进行兜底保护,确保在序列化的过程中不会意外泄露。

typescript 复制代码
// entities/user.entity.ts
import { Entity, Column, PrimaryGeneratedColumn } from 'typeorm';
import { ApiProperty } from '@nestjs/swagger';
import { Exclude } from 'class-transformer';

@Entity('users')
export class User {
  @PrimaryGeneratedColumn()
  id: number;

  @Column()
  username: string;

  // 默认序列化时永远排除,防止意外泄露
  @Exclude() 
  @Column()
  password: string;

  @ApiProperty({ description: '角色' })
  @Column({ default: 'user' })
  userRole: string; // 业务逻辑需要,但普通API不需要

  @Column()
  isActive: boolean;

  @Column()
  createdAt: Date;
}

2. 利用 Mapped Types 派生场景化 DTO

针对不同接口需求,从 Entity 中"挑选 "或"排除"字段生成 DTO,通过一次或多次映射类型的操作实现按需输入和输出,免去了手动复制粘贴属性的工作,且Swagger API文档保持同步。

typescript 复制代码
// dto/user.dto.ts
import { PickType } from '@nestjs/swagger';
import { User } from '../entities/user.entity';

// 场景A:普通用户视图(只看基本信息)
// PickType 会自动继承 User 中对应字段的 @ApiProperty 描述
export class PublicUserDto extends PickType(User, [
  'id', 
  'username'
] as const) {}

// 场景B:管理员视图(排除密码,保留其他所有)
// 即使 User 类有 password 字段,因为加了 @Exclude,这里其实双重保险
export class AdminUserDto extends PickType(User, [
  'id', 'username', 'userRole', 'isActive', 'createdAt'
] as const) {}

3. Controller 层进行转换与清理

Service 层返回完整的 User 实体,Controller 层使用 plainToInstance方法将其转换为特定的 DTO,并剔除多余字段。

需要配置 excludeExtraneousValues: true,这意味着"DTO 里没定义的字段,全部丢弃"。

typescript 复制代码
// user.controller.ts
import { Controller, Get } from '@nestjs/common';
import { plainToInstance } from 'class-transformer';
import { PublicUserDto } from './dto/user.dto';
import { UserService } from './user.service';

@Controller('users')
export class UserController {
  constructor(private readonly userService: UserService) {}

  @Get('list')
  async findAll(): Promise<PublicUserDto[]> {
    // 1. Service 返回的是完整的 User[] (包含 userRole, isActive, createdAt)
    const users = await this.userService.findAll();

    // 2. 转换为 PublicUserDto
    // excludeExtraneousValues: true 确保只保留 PublicUserDto 中定义的 'id' 和 'username'
    // 任何多余字段(如 userRole)都会被剔除
    return plainToInstance(PublicUserDto, users, {
      excludeExtraneousValues: true, 
    });
  }
}

总结

  1. Service 层 :只处理Entity,确保业务逻辑拥有完整数据。
  2. DTO 定义 :使用 PickType / OmitType 基于 Entity 生成,避免重复定义。
  3. Controller 层 :使用 plainToInstance + excludeExtraneousValues 清洗数据,实现接口的字段过滤。

这种模式既保证了内部逻辑的灵活性,又确保了对外接口的安全性和整洁性。 希望对您开发工作有帮助,欢迎点赞收藏评论。

相关推荐
钟智强5 小时前
React2Shell:CVE-2025-66478 Next.js 远程执行漏洞深度分析与代码剖析
开发语言·javascript·ecmascript
Dragon Wu5 小时前
Electron Forge集成React Typescript完整步骤
前端·javascript·react.js·typescript·electron·reactjs
华仔啊5 小时前
jQuery 4.0 发布,IE 终于被放弃了
前端·javascript
空白诗5 小时前
高级进阶 React Native 鸿蒙跨平台开发:slider 滑块组件 - 进度条与评分系统
javascript·react native·react.js
晓得迷路了6 小时前
栗子前端技术周刊第 116 期 - 2025 JS 状态调查结果、Babel 7.29.0、Vue Router 5...
前端·javascript·vue.js
How_doyou_do6 小时前
执行上下文、作用域、闭包 patch
javascript
叫我一声阿雷吧6 小时前
深入理解JavaScript作用域和闭包,解决变量访问问题
开发语言·javascript·ecmascript
iDao技术魔方6 小时前
深入Vue 3响应式系统:为什么嵌套对象修改后界面不更新?
javascript·vue.js·ecmascript
历程里程碑6 小时前
普通数组-----除了自身以外数组的乘积
大数据·javascript·python·算法·elasticsearch·搜索引擎·flask