如何在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 }
核心诉求
- 提升开发速度:团队有大量需要分页和/或排序的实体,希望减少重复代码
- 增强灵活性:比如未来要把
order_direction的可接受值从小写asc/desc改成大写ASC/DESC,不用逐个修改所有可排序DTO(这类自定义属性变更场景不止这一个) - 保证一致性:避免出现
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
相关产品推荐
相关产品推荐

