Why you should not use class-validator in NestJS!
Tags: nestjs, typescript, node, programming
Published: 2022-06-21 17:09:17

**Introduction**

I encountered NestJS for the first time about five years ago.

I was making a transition from .net to node js, so I was looking for a strongly-typed, well-documented server-side library.

I found NestJS.

The class validator decorators have always bothered me, and I even wrote a lint that forced programmers to add the validation.

We recently decided at my work to move to NestJS, so this issue has popped up again and I found a great solution!

Since we can use [third-party](https://www.npmjs.com/package/express-openapi-validator) libraries to do validation using OpenAPI documents, all we need to do is to “compile” Nest controllers to OpenAPI documents. Here we go!


## Let's start

There are two critical things to consider when writing APIs:

- **Documentation** - Your API must be understood by your consumers. There is a standard interface for that - https://swagger.io/specification/


- **Validation** - You don’t want to query the database or do something with some wrong input data.


First, we'll do it in the old primitive way, and then I'll show you my way.

We will create a simple route that queries events by time, type, and location.

### The Old Ugly Way

Create a new nest application - 
`nest new the-old-way`

Create Events controlller - 
`nest g resource Events`

(Do not generate CRUD entry points that will add a lot of code that we do not need.)

Let's add the events route:

```typescript
import { Controller, Get, Param, Query } from "@nestjs/common";

interface Event {
  timestamp: number;
  type: EventType;
  locationId: number;
  id: number;
}

enum EventType {
  SYSTEM = "system",
  USER = "user",
}

@Controller("events")
export class EventsController {
  @Get(":locationId")
  getEvents(@Param("locationId") locationId: number, 
            @Query("ids") ids?: number[], 
            @Query("type") type?: EventType): Promise<Event[]> {
    return new Promise((res) => res([]));
  }
}
```

**Location** path parm is a number and it is required.

**ids** query param is an array of numbers (optional).

**type** query param can be only "system" or "user"(optional).

The response is an array of events.

Let's add a validation pipe ([official doc](https://docs.nestjs.com/techniques/validation)) 
`npm i --save class-validator class-transformer`
In the main.ts file, add `app.useGlobalPipes(new ValidationPipe());`

Let's add an openAPI generator by following the [official article](https://docs.nestjs.com/openapi/introduction).

When you open http://localhost:3000/api/ you can see the schema: 

![Image description](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/ncrgoeq48qihkhaekrgh.png)

There are some problems here - 

1. Query parameters are mandatory,but they should be optional.

2. The type parameter is a string, but it should be an enum.

3. There is no response schema.

4. There is no validation - you can test this by sending a string in the locationId param - http://localhost:3000/events/some-string


When we compile to javascript, all the types disappear.  NestJS solves this problem by using decorators. 
So, let's try to fix this with ApiProperty and class validator decorators.

Let's try to fix that with ApiProperty and class validator decorators.

To use decorators, we first need to move our parameters to some class:

```typescript
class GetEventsPathParams {
 @IsNumber() locationId: number;
}

class GetEventsQueryParams {
 @IsArray() @IsOptional() ids?: number[];
 @IsEnum(EventType) @IsOptional() type?: EventType;
}

@Controller("events")
export class EventsController {
 @Get(":locationId")
 getEvents(@Param() params: GetEventsPathParams, @Query() query: GetEventsQueryParams): Promise<Event[]> {
   return new Promise((res) => res([]));
 }
}

```

Success!  Now we get a validation error -
`{"statusCode":400,"message":["locationId must be a number conforming to the specified constraints"],"error":"Bad Request"}`

Let's see what happened with the opanAPI doc -


![Image description](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/m4m0joq9fffsi7g2ergs.png)
Whoops! All our parameters were gone!


Let's try to fix this by adding ApiResponse decorators -

```typescript
class Event {
 @ApiProperty() timestamp: number;
 @ApiProperty() type: EventType;
 @ApiProperty() locationId: number;
 @ApiProperty() id: number;
}

enum EventType {
 SYSTEM = "system",
 USER = "user",
}

class GetEventsPathParams {
 @ApiProperty({ required: true }) @IsNumber() locationId: number;
}

class GetEventsQueryParams {
 @ApiProperty({ required: false, type: ["number"] }) @IsArray() @IsOptional() ids?: number[];
 @ApiProperty({ required: false, enum: EventType }) @IsEnum(EventType) @IsOptional() type?: EventType;
}

@Controller("events")
export class EventsController {
 @Get(":locationId")
 @ApiResponse({ type: [Event] })
 getEvents(@Param() params: GetEventsPathParams, @Query() query: GetEventsQueryParams): Promise<Event[]> {
   return new Promise((res) => res([]));
 }
}
```

And voila - it looks much better:

![Image description](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/bpd8jxl2wz6ua49ys5bc.png)

The point is clear:

- You need to declare each type 3 times!
- You need to learn class validator and @nestjs/swagger!
- You have to create classes that will never be instantiated!
- To allow the declaration of a class property without initialization, Nest sets the strict-flag to false.  In other words, it is less type-safe.

The solution for all this is to create a package that can “compile” NestJS controllers to OpenAPI documents. 

Feel free to look at my code on [Github](https://github.com/yantrab/nest-openapi-gen), any contributions or suggestions are welcome!

Let's start again from the beginning-

Create a new nest application - 
`nest new the-new-way`

Create Events controlller - 
`nest g resource Events`

Do not generate CRUD entry points that will add a lot of code that we do not need.

Let's add the events route:
```typescript
interface Event {
  timestamp: number;
  type: EventType;
  locationId: number;
  id: number;
}

enum EventType {
  SYSTEM = "system",
  USER = "user",
}

@Controller("events")
export class EventsController {
  @Get(":locationId")
  getEvents(@Param("locationId") locationId: number,
            @Query("ids") ids?: number[],
            @Query("type") type?: EventType): Promise<Event[]> {
    return new Promise(resolve => resolve([]));
  }
}
```


Generate openapidoc with nest-openapi-gen package -
Run `npm i -D nest-openapi-gen `
Add openapi.generator.ts file in the root - 

```typescript
import { generate } from "nest-openapi-gen";
generate({ filePath: "./openapi.json" });
```
					
Change the build script to generate the openAPI document - 
`"build": "ts-node openapi.generator.ts && nest build",`

Run `npm run build`

You can see the file generated in the folder.

The last step is to add some openAPI UI to show our document.
we can use swagger-ui-express for that
Run `npm i @nestjs/swagger  swagger-ui-express`
Add this code in main.ts file
```typescript
const document = JSON.parse(readFileSync("./openapi.json", { encoding: "utf-8" }));
SwaggerModule.setup("api", app, document);
```

Open http://localhost:3000/api and see the magic.


![Image description](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/5j564hgn34fwr9taftq8.png)

But wait, we aren’t finished yet, we still need to take care of the validation.

Since we have the API documentation, we can use it for validation.

Let's use express-openapi-validator for that:

Run `npm i express-openapi-validator`

Add middleware:
```typescript
app.use(OpenApiValidator.middleware({
 apiSpec: require('../openapi.schema.json'),
 validateResponses: true
}));
```
Add a global exception filter -
```typescript
@Catch()
export class GlobalExceptionFilter implements ExceptionFilter {
 catch(exception: Error & { context?: any }, host: ArgumentsHost) {
   const ctx = host.switchToHttp();
   const response = ctx.getResponse<Response>();
   const request = ctx.getRequest<Request>();
   const status =
     exception instanceof HttpException
       ? exception.getStatus()
       : exception instanceof BadRequest
       ? exception.status
       : HttpStatus.INTERNAL_SERVER_ERROR;

   response.status(status).json({
     statusCode: status,
     timestamp: new Date().toISOString(),
     path: request.url,
     message: exception.message,
     errors: (exception as BadRequest).errors,
   });
 }
}
```

And that's it!  Now we have validation and openapi documents for our code, without adding unnecessary classes and decorators.

One last thing, we need to set the strict-flag to true to avoid using properties without initializing.

Just add `"strict": true`, and remove "strictNullChecks": false,from your tsconfg file.

You can find the source code [here](https://github.com/yantrab/nest-openapi-gen/tree/main/examples)