I have different routes that use different DTOs for the payload requests. However, I have some input rules on the payloads, and I want to enforce those, without having to redefine them in my different DTOs. I also use the Swagger documentation and need to have the API Response decorated accordingly.
For instance, our payload may have an EntryDate, with is a number in the format YYYYMMDD. This obviously has rules, with a Minimum (20100101) and a Maximum (20991231) to allow daily entries from 2010 to 2099. When the date is in a payload, that DTO has the definitions:
EntryDateDTO:
export class EntryDateDTO {
@IsDefined() @IsNumber() @Min(20100101) @Max(20991231) public readonly EntryDate: number; // YYYYMMDD
constructor(EntryDate: number) {
this.EntryDate = EntryDate;
}
}
This works fine. Now I want to also have a Entry Date for the URL parameters, since we can reach the route via a direct endpoint: POST /api/TimeEntry/user1/20220909/
The above entry point works for placing the data in the body into that daily location, for 'user1'. The problem is, when I do the controller, I need to create a DTO for evaluating the parameters (userid: user1, entrydate: 20220909). This works as it can be a simple DTO with these fields.
This would allow me to create a UserEntryDateDTO that extends from TimeEntryDTO:
UserEntryDateDTO:
export class UserEntryDateDTO extends EntryDateDTO {
@IsDefined() @IsString() @MaxLength(20) @MinLength(3) public readonly UserId: string;
constructor(UserId: string, EntryDate: number) {
super(EntryDate);
this.UserId = UserId;
}
}
So far so good. However, as my API has gotten more complex, more routes, etc., I now have the need to extend (basically include) multiple other small DTOs. The EntryDateDTO is the best example of what might be needed in different routes, as the entry date limits will apply to any dates.
However, I do have additional routes that need the EntryDateDTO, but may also require some basic User information (UserIdDTO) which should have the same restrictions on the UserId, as the User Maintenance route, and others.
How to best handle the creation of small DTOs for Parameters, Queries, and use in multiple other DTO objects included in the body DTOs of different routes?
For instance: UserSchedulingDTO:
export class UserSchedulingDTO {
public readonly UserInfo: UserIdDTO;
public readonly ScheduleDate: TimeEntryDTO
@IsDefined() @IsString() @MaxLength(20) @MinLength(8) public readonly ProjectId: string;
constructor(ProjectId: string, UserId: string, EntryDate: number) {
this.ProjectId = ProjectId;
this.UserId = new UserIdDTO(UserId);
this.EntryDate = new EntryDateDTO(EntryDate);
}
}
The problem with this approach is that each DTO, has a data field in it, which then becomes the extra field name of the field in the parent DTO. For instance, to get the value from UserSchedulingDTO, to get the User, it is:
const UserSchedule: UserSchedulingDTO = new UserSchedulingDTO('Project1', 'user_id_1', 20220912);
console.log(`UserId: ${UserSchedule.UserId.UserId}`);
Ideally, I want to just be able to use UserSchedule.UserId
and have a basic getter pull the UserId from the definition. I am not aware of any way to create a basic class with a single getter method to return an internal value, without having to specify some sort of method name.
Then when I map the body into my DTO on the controller, my API fields look strange, as the UserId needs to be repeated as well.
{
"UserId": {
"UserId": "johndoe"
}
}
where I really simply want to have:
{
"UserId": "johndoe"
}