Understanding Data Transfer Objects (DTO) and Data Validation in TypeScript (NestJS)
Tags: nestjs, dto, datavalidation, classvalidator
Published: 2022-05-15 23:38:02

Data Transfer Objects (DTOs) are the basis of data validation in NestJS applications. DTOs enable various layers of flexible data validation in NestJS. 

In this publication, you will take a deep dive into data transfer objects, discussing validation mechanisms, authentication models, and everything else you need to know about data transfer objects.

## Prerequisites

This tutorial is not beginner-themed. If you’re just getting started with NestJS, read this [article](https://davidekete.hashnode.dev/the-abcs-of-nestjs-a-beginners-guide-with-mongodbmongoose).

You’ll need to meet these requirements to fully understand this article.

- Prior experience creating NestJS apps with MongoDB.
- Competence with PostMan (recommended) or any other API testing tool.
- Basic knowledge of bcrypt or the NodeJS crypto module.

Once you meet these requirements, we can get started.

## What is a Data Transfer Object?

A data transfer object, commonly called a DTO, is an object used to validate data and define data structure sent into your Nest applications. DTOs are similar to interfaces, but differ from interfaces in the following ways:

- Interfaces are used for type-checking and structure definition.
- A DTO is used for type-checking, structure definition, and data validation.
- Interfaces disappear during compilation, as it’s native to TypeScript and doesn't exist in JavaScript.
- DTOs are defined using classes that are supported in native JavaScript. Hence, it remains after compilation.

DTOs alone can only do type-checking and structure definition. To run data validations using DTOs, you’ll need to use the NestJS `ValidationPipe`.

## Validation Mechanisms

Pipes are used to validate data in NestJS. Data passing through a pipe is evaluated, if it passes the validation test, it is returned unmodified; otherwise, an error is thrown. 

NestJS has 8 inbuilt pipes, but you’ll be focusing on the `ValidationPipe` which makes use of the `class-validator` package, because it abstracts a lot of verbose code and makes it easy to validate data using decorators.

## DTOs in a Simple User Authentication Model

A user authentication model is a perfect example to demystify DTOs. This is because it requires multiple levels of data validation. Let’s make one and explore DTOs and the `ValidationPipe` to validate all forms of data coming into our application.

### Setting up Development Environment

To set up your development environment:

- Generate a new project using the Nest CLI,
- Generate your `Module`, `Service`, and `Controller` using the CLI,
- Install `Mongoose` and connect your application to the database,
- Create your schema folder and define the `Schema`.

A typical user-auth schema should look like:

```tsx
import { Prop, Schema, SchemaFactory } from '@nestjs/mongoose';
import { Document } from 'mongoose';

export type UserDocument = User & Document;

@Schema()
export class User {
  @Prop({ required: true })
  fullName: string;

  @Prop({ required: true })
  email: string;

  @Prop({ required: true })
  password: string;
}
export const UserSchema = SchemaFactory.createForClass(User);
```

Having the `fullName`, `email`, and `password` as required properties.

- Create a folder inside your module and call it `dto`. This is where your dto classes will be stored and exported.
- Create a file inside your `dto` folder and name it `user.dto.ts`.

## Structure of a DTO

A DTO is a class, so it follows the same syntax a class does.

```tsx
export class newUserDto {
  fullName: string;
  email: string;
  password: string;
}
```

Above is the structure of a basic DTO.

This structure is only useful for type-checking, it has no data validation properties.

## Implementing Data Validation

To add validation functions; follow the steps below;

- Inside your `main.ts`,
- Import `{ValidationPipe}` from `'@nestjs/common'`,
- Inside the `bootstrap` function and directly below the `app` constant, call the `useGlobalPipes` method on `app` and pass in `new ValidationPipe()` as an argument.

```tsx
import { NestFactory } from '@nestjs/core';
import { AppModule } from './app.module';
import { ValidationPipe } from '@nestjs/common';
import * as dotenv from 'dotenv';
dotenv.config();

async function bootstrap() {
  const app = await NestFactory.create(AppModule);
  app.useGlobalPipes(new ValidationPipe());
  await app.listen(process.env.PORT);
}
bootstrap();
```

This will enable auto-validation and ensure that all endpoints are protected from receiving incorrect data. The Validation pipe takes in an options object as an argument, you can find out more about it in the [official documentation](https://docs.nestjs.com/techniques/validation#overview).

- Install the `class-validator` and the `class-transformer` package by running:

```tsx
npm i --save class-validator class-transformer
```

The `class-transformer` converts JSON objects into an instance of your DTO class and vice-versa.

The `class-validator` package has a lot of validation queries, which you can find in their [documentation](https://www.npmjs.com/package/class-validator), but you’ll be focusing on a few of them related to user data validation. In your `user.dto.ts` file, import the following queries from `class-validator`:

- `IsNotEmpty`: This validator checks if a given value is empty. It takes in an object of options as an argument.
- `IsString`: This validator checks if a given value is a real string. It takes in an object of options as an argument.
- `IsEmail`: This validator checks if a given value is of type email. It takes in an object of options as an argument.
- `Length`: This Validator takes in 3 arguments. The first is the `minLength`, then the `maxLength`, and finally an object of options.

The above will be used as decorators to validate their respective fields. Implement this in your DTO,

```tsx
import { IsNotEmpty, IsEmail, Length, IsString } from 'class-validator';

export class newUserDto {
  @IsNotEmpty({ message: 'Please Enter Full Name' })
  @IsString({ message: 'Please Enter Valid Name' })
  fullName: string;

  @IsEmail({ message: 'Please Enter a Valid Email' })
  email: string;

  @Length(6, 50, {
    message: 'Password length Must be between 6 and 50 charcters',
  })
  password: string;
}

```

The password field has only a `Length` validation but in production, you might need to add a few more fields to ensure the password is more secure. 

These new validation fields might include checking if the password contains uppercase, lowercase, and numbers. 

This can be done in several ways, including:

- Padding with more validation decorators, like you did in the `fullName` field.
- Using the `@Matches` validator and passing a regular expression that checks for your requirements as an argument.

But the cleanest way to do this would be by creating a custom validator to suit your specifications. You can learn more about custom decorators [here](https://github.com/davidekete/class-validator#custom-validation-decorators).

## Post & Put Requests

Test the data validation by making a few `POST` and `PUT` requests to your application. But before that, it’s bad practice to store your passwords in plain text. So hash them first before storing them in the database;

### Hashing Passwords

You can hash the passwords using a package called `bcrypt` or the NodeJS crypto module. This publication covers `bcrypt`. To install `bcrypt` run

```bash
npm i bcrypt
npm i -D @types/bcrypt
```

Create a `utils` folder to store files that contain utility functions, like the function you’ll use to hash the passwords.

To ensure that user passwords are protected, you’ll need to hash them before they are stored in the database. Let’s implement this

- Create a file in your `utils` folder, this will contain your utility functions.
- Import `*` as `bcrypt` from `'bcrypt'`
- Create a function that takes in the raw password and hashes it using `bcrypt` then returns the hashed password. Export the function.

```tsx
import * as bcrypt from 'bcrypt';

export async function hashPassword(textPassword: string) {
  const salt = await bcrypt.genSalt();
  const hash = await bcrypt.hash(textPassword, salt);
  return hash;
}
```

There are different ways to use the `bcrypt` package, which you can find [here](https://www.npmjs.com/package/bcrypt).

Now that you have a function to hash our passwords, Implement your first `POST` request.

### Creating a New User

To create a new user with valid credentials, you’ll need to import the DTO you created earlier into your service. 

**Service Logic**

- Create an `async` function `newUser` that takes in a parameter `user` which should be the data of the new user. Set its type to the DTO created earlier.
- Import the `hashPassword` function.
- Inside the `async` function, create a constant `password`, and assign the return value of awaiting the `hashPassword` function with `user.password` as an argument.
- Return a new instance of your own version of my `userModel` as an argument pass in an object containing a destructured `user` and the `password`, and call the `save()` method on it.

```tsx
import { Injectable } from '@nestjs/common';
import { Model } from 'mongoose';
import { InjectModel } from '@nestjs/mongoose';
import { User, UserDocument } from './schemas/user.schema';
import { newUserDto } from './dto/user.dto';
import { hashPassword } from './utilis/bcrypt.utils';

@Injectable()
export class AuthService {
  constructor(
    @InjectModel(User.name)
    private userModel: Model<UserDocument>,
  ) {}

  async newUser(user: newUserDto): Promise<User> {
    const password = await hashPassword(user.password);
    return await new this.userModel({ ...user, password }).save();
  }
}
```

The DTO and the validators we set up earlier will continuously check if the data is valid before storing the user data in the database. 

Implement the controller logic so that you can test the DTO with some dummy data.

**Controller Logic**

```tsx
import {
  Controller,
  Body,
  Post, 
} from '@nestjs/common';
import { AuthService } from './auth.service';
import { newUserDto } from './dto/user.dto';

@Controller('users')
export class AuthController {
  constructor(private readonly service: AuthService) {}

  @Post('signup')
  async createUser(
    @Body()
    user: newUserDto,
  ): Promise<newUserDto> {
    return await this.service.newUser(user);
  }
```

As you can see in the code block above, the `user` parameter has a type of `newUserDto`. This ensures that all data coming into the application matches the DTO, else it throws an error.

### Testing Endpoints

Test the validation with some dummy data by making requests to `http://localhost:3000/users/signup` using PostMan or your preferred testing tool.

```json
{
    "fullName":"Jon Snow",
    "email":"snow@housestark.com",
    "password":"password"
}
```

The JSON data above checks all validation boxes. Hence, you’ll get a `201` response with data, with an `_id` property and a hashed password. Copy the `_id` property, you’ll need it when testing data validation in `PUT` requests.

```json
{
    "fullName":"Arya Stark",
    "email":"aryathefaceless@housestark",
    "password":"password"
}
```

The JSON data above has an invalid `email` property. Hence, you’ll get a `400` response with a message that describes the error. Save it to the database by updating the `email` property to `aryathefaceless@housestark.com`.

```json
{
    "fullName":"Tyrion Lannister",
    "email":"tyrion@houselannister.com",
    "password":"pass",
     "house":"Lannister"
}
```

The JSON data above has 2 problems, the password is too short (less than 6 characters) and there is an extra field `house`. 

Increase the length of the `password` and send the request again. You’ll get a `200` request with the processed data, but the `house` field isn’t stored in the database because it was filtered when it was passed through the validation pipe.

### Updating a User

Implement a `PUT` route to update user data when required.

**Service Logic**

```tsx
async updateUser(id: string, userData: newUserDto): Promise<User> {
    return await this.userModel.findByIdAndUpdate(id, userData);
  }
```

**Controller Logic**

```tsx
@Put(':id')
  async updateuser(
    @Param('id')
    id: string,
    @Body()
    user: newUserDto,
  ): Promise<newUserDto> {
    return this.service.updateUser(id, user);
  }
```

Similar to the `POST` request, the `user` is given a type of `newUserDto`. Hence, all data is checked thoroughly before it is saved in the database.

Test this endpoint by updating one of the users stored in your database. Recall that you copied the `_id` belonging to Jon Snow.

So make a `PUT` request to `http://localhost:3000/id` with the following data;

```json
{
    "fullName":"Jon Snow",
    "email":"snow@housetargaryen.com",
    "password":"password"
}
```

The data above checks all validation boxes, so it would return a `200` response. If any of the data fields contain invalid data, it would return a `400` response and the `PUT` request would fail.

### Logging Users in

A user-authentication model wouldn't be complete if you couldn't log users in. To implement the logic for that;

Firstly, you’ll need a `bcrypt` utility function that will compare the hashed and plain text passwords. Like so,

```tsx
export async function validatePassword(textPassword: string, hash: string) {
  const validUser = await bcrypt.compare(textPassword, hash);
  return validUser;
}
```

Then, you’ll have to create a new DTO for the login data. The DTO is similar to the `newUserDto` but without the `fullName` property.

```tsx
import { IsEmail } from 'class-validator';

export class loginUserDto {
  @IsEmail({ message: 'Please Enter a Valid Email' })
  email: string;

  password: string;
}
```

Notice that no validator was added to the `password` property. This is because it could pose a security threat in the future, as it exposes the password length range to malicious users.

**Service Logic**

```tsx
async loginUser(loginData: loginUserDto) {
    const email = loginData.email;
    const user = await this.userModel.findOne({ email });
    if (user) {
      const valid = await validatePassword(loginData.password, user.password);
      if (valid) {
        return user;
      }
    }
    return new UnauthorizedException('Invalid Credentials');
  }
```

Your service logic should be able to,

Check if the user exists;

- If the user exists, validate the passwords
- If the user doesn't exist, throw an exception

Validate Passwords;

- If the passwords match, return the user
- If the passwords don’t match, throw an exception

**Controller Logic**

```tsx
@Post('login')
  async loginUser(
    @Body()
    loginData: loginUserDto,
  ) {
    return await this.service.loginUser(loginData);
  }
```

Your controller should make the `POST` request to `http://localhost:3000/users/login`.

## Conclusion

You’re finally at the end of this article. Here’s a recap what you’ve covered.

- What a DTO is,
- Differences between a DTO and an interface,
- Structure of a DTO,
- NestJS validation mechanism,
- Making a simple user-authentication model with bcrypt.

That’s quite a lot, congratulations on making it this far.

You can find the code on [Github](https://github.com/davidekete/Data-Transfer-Objects).

Happy Coding!