You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

如何在NestJS中复用基础DTO类组合多端点查询DTO?

复用基础DTO类实现查询DTO的方案探讨

问题背景

我有以下几个API端点:

GET /api/invoices?page=1&limit=10&id=2022001
  • 支持分页(page、limit)
  • 支持自定义筛选(id)
GET /api/users?order_by=first_name&order_direction=asc
  • 支持排序(order_by、order_direction)
GET /api/products?page=1&limit=10&order_by=created_at&order_direction=asc&description=lorem
  • 支持分页(page、limit)
  • 支持排序(order_by、order_direction)
  • 支持自定义筛选(description)

我希望创建可复用的基础DTO类来实现各个端点的查询DTO,同时要兼容OpenAPI文档生成,而且不想用TS Mixins(觉得使用繁琐)。

理想的基础DTO设计

我预想的通用基础DTO类结构如下:

import { IsInt, IsEnum, IsString } from 'class-validator'

class PaginationDto {
  @IsInt()
  limit: number

  @IsInt()
  page: number
}

class OrderDto {
  @IsEnum(['asc', 'desc'])
  order_direction: 'asc' | 'desc'

  /**
   * 最好能使用自定义枚举,因为每个实体可排序的字段不同
   */
  @IsString()
  order_by: string 
}

但在尝试用端点专属DTO继承这些基础类时,遇到了几个问题:

import { IsInt, IsString } from 'class-validator'
import { PaginationDto, OrderDto } from '@shared/base.dto'

/**
 * 这个可以正常工作
 */
class GetInvoicesQueryDto extends PaginationDto {
  @IsInt()
  id: number
}

/**
 * 这个能运行,但我希望当'order_by'不在允许的字段列表(如'first_name'和'created_at')时校验失败
 */
class GetUsersQueryDto extends OrderDto {}

/**
 * 这个无法工作,因为类只能继承单个父类
 *
 * 未来我可能需要继承更多类
 */
class GetProductsQueryDto extends PaginationDto, OrderDto {
  @IsString()
  description: string
}

核心诉求

  1. 提升开发速度:团队有大量需要分页和/或排序的实体,希望减少重复代码
  2. 增强灵活性:比如未来要把order_direction的可接受值从小写asc/desc改成大写ASC/DESC,不用逐个修改所有可排序DTO(这类自定义属性变更场景不止这一个)
  3. 保证一致性:避免出现order_direction和orderDirection这类命名不一致的情况

解决方案

1. 用NestJS的IntersectionType解决多继承问题

NestJS的Swagger工具包提供了IntersectionType工具类,可以合并多个DTO类,完美解决单继承限制,同时自动兼容OpenAPI文档生成。

基础DTO改造

先把基础DTO定义为可复用的类,排序DTO设为抽象类,方便子类指定具体的可排序字段枚举:

// src/shared/base.dto.ts
import { IsInt, IsEnum, IsOptional } from 'class-validator'
import { ApiProperty, ApiPropertyOptional } from '@nestjs/swagger'
import { Type } from 'class-transformer'

// 分页基础DTO:添加默认值和类型转换
export class PaginationDto {
  @ApiPropertyOptional({ description: '每页数量', example: 10, default: 10 })
  @IsOptional()
  @IsInt()
  @Type(() => Number)
  limit: number = 10

  @ApiPropertyOptional({ description: '页码', example: 1, default: 1 })
  @IsOptional()
  @IsInt()
  @Type(() => Number)
  page: number = 1
}

// 排序基础DTO:抽象类,让子类实现具体的排序字段枚举
export abstract class OrderDto<T extends string> {
  @ApiPropertyOptional({ description: '排序方向', enum: ['asc', 'desc'], example: 'asc', default: 'asc' })
  @IsOptional()
  @IsEnum(['asc', 'desc'])
  order_direction: 'asc' | 'desc' = 'asc'

  // 子类会覆盖这个装饰器,指定当前实体的可排序字段
  @ApiProperty({ description: '排序字段', enum: [] })
  @IsEnum([])
  order_by: T
}

实体专属枚举与DTO实现

为每个实体创建可排序字段的枚举,然后用IntersectionType合并基础DTO:

// src/users/dto/enums/user-sort-fields.enum.ts
export enum UserSortFields {
  FIRST_NAME = 'first_name',
  LAST_NAME = 'last_name',
  CREATED_AT = 'created_at'
}

// src/users/dto/get-users-query.dto.ts
import { OrderDto } from '@shared/base.dto'
import { UserSortFields } from './enums/user-sort-fields.enum'
import { IsEnum } from 'class-validator'
import { ApiProperty } from '@nestjs/swagger'

export class GetUsersQueryDto extends OrderDto<UserSortFields> {
  @ApiProperty({ description: '排序字段', enum: UserSortFields, example: UserSortFields.FIRST_NAME })
  @IsEnum(UserSortFields)
  order_by: UserSortFields
}

// src/products/dto/enums/product-sort-fields.enum.ts
export enum ProductSortFields {
  NAME = 'name',
  CREATED_AT = 'created_at',
  PRICE = 'price'
}

// src/products/dto/get-products-query.dto.ts
import { IsString } from 'class-validator'
import { PaginationDto, OrderDto } from '@shared/base.dto'
import { IntersectionType } from '@nestjs/swagger'
import { ApiProperty } from '@nestjs/swagger'
import { ProductSortFields } from './enums/product-sort-fields.enum'

// 合并分页和排序DTO
export class ProductBaseQueryDto extends IntersectionType(PaginationDto, OrderDto<ProductSortFields>) {
  @ApiProperty({ description: '排序字段', enum: ProductSortFields, example: ProductSortFields.CREATED_AT })
  @IsEnum(ProductSortFields)
  order_by: ProductSortFields
}

// 添加自定义筛选字段
export class GetProductsQueryDto extends ProductBaseQueryDto {
  @ApiProperty({ description: '产品描述筛选', example: 'lorem' })
  @IsString()
  description: string
}

// 发票DTO保持原有继承方式即可
// src/invoices/dto/get-invoices-query.dto.ts
import { IsInt } from 'class-validator'
import { PaginationDto } from '@shared/base.dto'
import { ApiProperty } from '@nestjs/swagger'

export class GetInvoicesQueryDto extends PaginationDto {
  @ApiProperty({ description: '发票ID', example: 2022001 })
  @IsInt()
  id: number
}

2. 满足核心诉求的细节优化

  • 提升开发速度:基础DTO的分页、排序逻辑只写一次,所有实体DTO直接复用,新增实体时只需定义专属枚举和筛选字段
  • 增强灵活性:全局规则(如order_direction的可选值)直接在OrderDto中修改,所有子类自动生效;实体专属规则(如可排序字段)通过枚举维护,修改只需更新枚举
  • 保证一致性:所有通用字段名(page、limit、order_direction)都在基础DTO中统一定义,子类无法随意修改,从根源避免命名混乱

内容的提问来源于stack exchange,提问作者Lisumio

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.08.19 12:20:28