In this post I’ll argue that NestJs in its default settings isn’t actually type-safe which in my opinion makes it a bad Typescript backend framework.

I’d like to preface this rant with big respect to maintainers of both NestJS and class-validator library. They’re absolutely awesome for maintaining those great open source libraries which are just ultimately limited by the environment they’re being used in.

When you decide to break out of the simpleness of the Express framework towards something that’s safer, more typed and structured you’ll most likely find NestJS.

I’d even say it’s probably the most popular backend framework for NodeJS after Express (which is not really a framework, just a library, I know). It has tons of stars and activity, its development is active and your chances of finding devs at least partially familiar with it are pretty high.

It’s primarily TypeScript, looks a lot like Spring Boot and has all the features you’d want from professional backend framework.

Sounds good right? Well - it has one major issue which is really hard to get around, it’s class and decorator based and classes and decorators in TypeScript suck.

class-validator is not type-safe

Lets start with the recommended defaults, class-validator for validation of incoming data. I’ll cut through the chase and show you completely valid typescript code that also show the biggest weakness of this library:

export class UserDto
{
  @IsNumber()
  firstName?: string

  @IsDate()
  @IsNullable()
  @IsOptional()
  lastName: string
}

Typescript will just accept this, no complaints.

Why is that? Why can you just write such blatantly wrong code without any type errors? Well that’s because decorators in Typescript aren’t typed, at all. You simply can’t specify that some decorator can only be applied to a specific field type. You can’t really specify anything about the decorator target.

This is the core issue with NestJS, decorators suck in Typescript and there’s no way around it.

class-validator is clunky

I’ll again go straight to the point again, lets say you have an array of nested objects in your object:

class NestedDto
{}

class MainDto
{
  @IsArray()
  @IsObject({ each: true })
  @Type(() => NestedDto)
  @ValidateNested()
  nested1: NestedDto[]

  @IsObject({ each: true })
  @Type(() => NestedDto)
  @ValidateNested()
  nested2: NestedDto[]

  @IsArray()
  @IsObject({ each: true })
  @ValidateNested()
  nested3: NestedDto[]

  @IsArray()
  nested4: NestedDto[]
}

Can you tell me which of these is the correct way to define this? Is ValidateNested required together with IsObject({ each })? Is the Type needed? Do you really need 4 separate decorators to define this?

Sure there is one correct way somewhere in that DTO, but you can be sure that devs will just mix and match these as they want and you’ll be busy enforcing the correct way.

And I can’t even provide you with the correct answer without actually testing the code, the single-page scrollbar-minifying docs on github don’t answer this question.

If you’re reading this and you’re unfortunate enough to be dealing with this, I’d recommend writing your own decorator for this which joins alls these into something more manageable.

And if you’re reading this before you’ve started using NestJS, please take a look at alternatives for data validation and transforming. But if you also need OpenAPI output, be sure to make sure the alternative supports that too.

Decorators are experimental in typescript

For NestJS to work properly you need to enable those two flags in your Typescript configuration:

{
  // Enables decorators in Typescript
  "experimentalDecorators": true,
  // Enables emitting basic type info about properties into resulting JS code
  // Theoretically NestJS could work without this if you manually specified every type, but it would be pain
  "emitDecoratorMetadata": true
}

Both are experimental and both can be changed or deprecated at any time. Some alternative typescript compilers also don’t support these at all.

The experimentalDecorators which is required for NestJS to work is actually in danger of being deprecated because Javascript recently got official support for decorators and they work differently. For example they currently don’t support decorating method arguments, which is pretty crucial part of NestJS. See this section of proposal TC39.

emitDecoratorMetadata is bad

The idea behind emitDecoratorMetadata sounds good on paper. It emits type data into the compiled JS output so you can do basic runtime reflection. But it’s actually kind of terrible, it breaks at first sight of trouble and causes unexpected imports in the generated JS output.

For example, lets have this standard controller definition:

import { DataDto } from './data.dto'
import { ResultDto } from './result.dto'

class Controller
{
    post(@Body() data: DataDto, @Query() param: number): ResultDto
    {
        return { id: 1 }
    }
}

Transpiles into this JS code:

import { DataDto } from './data.dto'
import { ResultDto } from './result.dto'

class Controller {
    post(data, param) {
        return { id: 1 };
    }
}
__decorate([
    __param(0, Body()),
    __param(1, Query()),
    __metadata("design:type", Function),
    __metadata("design:paramtypes", [DataDto, Number]),
    __metadata("design:returntype", ResultDto)
], Controller.prototype, "post", null);

Thats nice, you have your param types, your return types and you can use all that for your logic. For example NestJS will infer what Dto should be used to validate the incoming body.

Now lets enhance it a bit. For one, you’ll most likely do something asynchronous in the operation so we need to adjust the return type to Promise<ResultDto>, secondly we want to make the query parameter is optional. You can either change the parameter to be optional or do an union with undefined. For sake of this demonstration we’ll do the latter.

class Controller
{
    post(@Body() data: DataDto, @Query() param: number | undefined): Promise<ResultDto>
    {
        return { id: 1 }
    }
}
class Controller {
    post(data, param) {
        return { id: 1 };
    }
}
__decorate([
    __param(0, Body()),
    __param(1, Query()),
    __metadata("design:type", Function),
    __metadata("design:paramtypes", [DataDto, Object]),
    __metadata("design:returntype", Promise)
], Controller.prototype, "post", null);

Aaand it’s gone. You’ve just lost all your metadata and you didn’t even know it. The return type and the second param type are completely useless now. Do you think NestJS will complain that the query param is missing the metadata? No, it’ll just quietly ignore it and allow user to supply whatever they want.

Also not visible in this example is that when you’re importing these dtos in typescript it seems like you’re only importing the type but since the resulting JS code refers to the class, it will also import the whole class leading to unexpected circular dependencies.

Generating OpenAPI schema requires compromises

NestJS also has first-class support for generating OpenAPI schema for your controllers. But given all the issues above it’s also kind of clunky.

You have basically two options:

  1. Put @ApiProperty decorator with manual swagger definition above every field of your DTOs
  2. Use Typescript compiler plugin provided by NestJS that will automatically adds those to all files that end with .dto.ts (configurable)

Option 1 is clunky because you again have absolutely no type safety, you can write anything into the decorator and no tool will complain about it.

Option 2 is clunky since you’re effectively locked into using official TSC (and I would be surprised if there’ll be support for this in the upcoming native TSC) and it’s hard to tell what the result will be, which class-validator patterns will actually be applied into the Swagger output. And if you have something slightly complicated, you’ll have to fall back to manual ApiProperty. Still this option works pretty good and if you’re fine with TSC there isn’t much to complain about.

Conclusion

NestJS is a bad framework if you’re looking for type safety out of the box, but you can work around it. If you don’t need all the other features NestJS provides, I’d suggest to look somewhere else since there are frameworks and libraries that do much better job at type safety.

Honorable mentions:

  • tRPC - if your client is also in Typescript, this will make type safety much more easier
  • oRPC - similar as above but with first-class OpenAPI support which makes integrating non-JS clients much easier
  • Fastify, Hono, etc

Appendix: Working around it

Here’s what I did to make NestJS more type safe:

  1. Created an universal custom ApiField decorator which various type options which:
  • provides proper validation with single switch (for example { type: 'object', array: true, schema: NestedDTO })
  • provides proper OpenAPI info based on its options
  1. Created a custom eslint rule which will check the typescript type against options inside ApiField. I’ve also implemented “fix” for this rule so I can just write the properties and the eslint will just fill in the field decorators automatically.

This way, you get at least some type safety when defining your DTOs and you don’t need to rely on TSC to properly decorate them using a compiler plugin.