I have personally come to the conclusion that typescript should not be used for writing a backend. The lack of RTTI means that you have to either write a TON of verbose code mapping Typescript class fields (note: different than ES6 classes, which would be more useful in this regard) to SQL statements.

What this means in practice is using ORM frameworks which require you to put decorators (which are still somehow "experimental" despite all major frameworks relying on them) on your class fields to generate sql commands.

But you also need to handle user input, and validate it. You can use JSONSchemas in some frameworks (fastify), but there's no really good way to generate these automatically yet, and you'll need to update them as you add more fields. This is most easily done through (a lot) more decorators

Additionally, because there's no RTTI, you can't rely on the typescript operators like keyof to generate other fields at runtime (only at compile time). Let's say you have a query interface you want to write. You can provide the name of a field to filter by, along with a value. This is a JSON POST Body. Maybe you have a model like:

export class BlogPost {
title: string;
body: string;
postedOn: Date;
postedBy: string;
}

To query the query interface, you'll need to declare every field again independently, because you can't apply class validators to the results of keyof operators:

export class QueryBlogPost {
@ApiProperty()
@IsString()
@IsOptional()
title?: string;

@ApiProperty()
@IsString()
@IsOptional()
body?: string;

@ApiProperty()
@IsString()
@IsDateString()
postedOn?: Date;

@ApiProperty()
@IsString()
@IsOptional()
postedBy?: string;
}

let's say you want to specify all the fields which you want returned, so you can limit the size of the response (perhaps you don't want to return the blog post body in a search request). Again, you cannot use keyof here to get a list of valid fields at runtime. Instead, you need to specify them manually:

export class QueryBlogPost {
@ApiProperty()
@IsString()
@IsOptional()
title?: string;

@ApiProperty()
@IsString()
@IsOptional()
body?: string;

@ApiProperty()
@IsString()
@IsDateString()
postedOn?: Date;

@ApiProperty()
@IsString()
@IsOptional()
postedBy?: string;

@ApiProperty()
@IsOptional()
@IsEnum(["title", "body", "postedOn", "postedBy"], {each: true})
fields?: string[]
}

If you add a new field, remember to manually add that field to your enum

Don't forget non-primitive types: if you have a nested class, you need to add multiple decorators for that, since you can't get the constructor or type of the class at runtime:

export class Author {
@ApiProperty()
@IsString()
firstName: string;

@ApiProperty()
@IsString()
lastName: string;
}

export class QueryBlogPost {
//...
@ApiProperty()
@ValidateNested()
@Type(() => Author)
@IsOptional()
author?: Author;
}

not to mention if it's an array, in which case every class needs @IsArray(), @ValidateNested({each: true}) added to it

Plus all the documentation decorators which add example queries, queries which take arrays (for operators such as "any" or "in".

This isn't to mention return type DTOs, or the decorators for the ORM.

Typescript is a frontend language. It is good for creating data. It is very bad for receiving it. With typescript as a backend, you end up writing the same code 5 times which introduces bugs. Do not write typescript backends just because your junior devs are afraid to learn Java or C# or a language more suited for backend development