DEV Community

depak379mandal
depak379mandal

Posted on

Validation and controller setup in Nest JS

Love to work with you, You can hire me on Upwork.

We have gone from defining first phase requirement to config load and TypeORM setup with migrations. Now we will look into setting up validation serialization and controller with Swagger Decorator’s.

We are starting with auth module, and we are going to create email controller instead of auth controller as we in future will add more services and controller for authentication provider like google, Facebook and LinkedIn. We already discussed APIs in getting started article, We are implementing those in here for controller and validation setup.

So I will just add controller with some comments and later will be explaining in details.

// src/modules/auth/email.controller.ts

import { Body, Controller, HttpCode, HttpStatus, Post } from '@nestjs/common';
import {
  ApiAcceptedResponse,
  ApiCreatedResponse,
  ApiForbiddenResponse,
  ApiNoContentResponse,
  ApiNotFoundResponse,
  ApiOperation,
  ApiTags,
} from '@nestjs/swagger';
import {
  EmailVerifyDto,
  LoginDto,
  RegisterDto,
  SendVerifyMailDto,
} from './email.dto';
import { EmailService } from './email.service';

// This is for swagger to aggregate all the APIs under this tag
@ApiTags('Auth Email')
// auth/email is path and it will get suffixed with /v1 as /v1/auth/email
@Controller({
  path: 'auth/email',
  version: '1',
})
export class EmailController {

  // It will be used later right now we are just 
  // going to return whatever we are getting from body
  constructor(private emailService: EmailService) {}

  @Post('/register')
  @ApiOperation({ summary: 'Register by email' })
  @ApiCreatedResponse({
    description: 'User successfully registered.',
  })
  // this actually overrides the default status code 
  @HttpCode(HttpStatus.CREATED)
  async register(@Body() registerDto: RegisterDto) {
    return registerDto;
  }

  @Post('/verify')
  @ApiOperation({ summary: 'Verify Email address.' })
  @ApiAcceptedResponse({
    description: 'Email verified successfully.',
  })
  @HttpCode(HttpStatus.ACCEPTED)
  async verify(@Body() emailVerifyDto: EmailVerifyDto) {
    return emailVerifyDto;
  }

  @Post('/login')
  @ApiOperation({ summary: 'Log in with Email.' })
  @HttpCode(HttpStatus.OK)
  async login(@Body() loginDto: LoginDto) {
    return loginDto;
  }

  @Post('/send-verify-email')
  @ApiOperation({ summary: 'Send Verification mail.' })
  @ApiNoContentResponse({
    description: 'Sent Verification mail.',
  })
  @ApiForbiddenResponse({
    description: 'User already verified.',
  })
  @ApiNotFoundResponse({
    description: 'User not found.',
  })
  @HttpCode(HttpStatus.NO_CONTENT)
  async sendVerifyMail(@Body() sendVerifyMailDto: SendVerifyMailDto) {
    return sendVerifyMailDto;
  }

  @Post('/reset-password-request')
  @ApiOperation({ summary: 'Send Reset Password mail.' })
  @ApiNoContentResponse({
    description: 'Sent Reset Password mail.',
  })
  @ApiForbiddenResponse({
    description: 'Please verify email first.',
  })
  @ApiNotFoundResponse({
    description: 'User not found.',
  })
  @HttpCode(HttpStatus.NO_CONTENT)
  async sendForgotMail(@Body() sendForgotMailDto: SendVerifyMailDto) {
    return sendForgotMailDto;
  }
}
Enter fullscreen mode Exit fullscreen mode
// src/modules/auth/email.service.ts

import { Injectable } from '@nestjs/common';

@Injectable()
export class EmailService {
  constructor() {}
}
Enter fullscreen mode Exit fullscreen mode

Have added some comments in the above code for understanding details of the code that are included. All the decorator starts with @Api are swagger decorators that generate code for us. So to define methods of the API we use @Get @post @Patch @head @Delete and @Put Decorators, they are very conveniently available from NestJS common library. You have to provide path, it automatically merges the base path from Controller decorator as suffix and starts working. You can also observer I have included emailService that we will discuss in the next article, But DTO’s will be in this article. DTO’s are for validation they are classes that have class-validator decorators and swagger decorators that help us the Body to validate and in same time it works as our swagger documentation.

You will see many decorators to even fetch some common data from request object or even Request itself. NestJS provide very convenient decorators for all those things. Like in above, I have used @Body decorator that actually gives me back body object from request. I can also fetch single field from body as @Body('email') previous code only fetches email from body, nothing else. We can also validate a particular body by providing a class with Validation Pipe. A Validation Pipe we are going to implement, but that will be global, not for every endpoint. So Validation Pipe Will take care of everything.

We need an endpoint for swagger too, So first we need all the DTO we have imported from email.dto.ts files and global validation pipe with swagger setup for our application.

// src/modules/auth/email.dto.ts

import { ApiProperty } from '@nestjs/swagger';
import { Transform } from 'class-transformer';
import {
  IsEmail,
  IsNotEmpty,
  IsString,
  IsStrongPassword,
} from 'class-validator';

const strongPasswordConfig = {
  minLength: 8,
  minLowercase: 1,
  minNumbers: 1,
  minSymbols: 1,
  minUppercase: 1,
};

export class RegisterDto {
  @ApiProperty({ example: 'example@danimai.com' })
  @IsEmail()
  @Transform(({ value }) =>
    typeof value === 'string' ? value.toLowerCase() : value,
  )
  email: string;

  @ApiProperty({ example: 'Password@123' })
  @IsString()
  @IsStrongPassword(strongPasswordConfig)
  password: string;

  @ApiProperty({ example: 'Danimai' })
  @IsString()
  @IsNotEmpty()
  first_name: string;

  @ApiProperty({ example: 'Mandal' })
  @IsString()
  @IsNotEmpty()
  last_name: string;
}

export class EmailVerifyDto {
  @ApiProperty({ example: 'vhsbdjsdfsd-dfmsdfjsd-sdfnsdk' })
  @IsString()
  verify_token: string;
}

export class LoginDto {
  @ApiProperty({ example: 'example@danimai.com' })
  @IsEmail()
  @Transform(({ value }) =>
    typeof value === 'string' ? value.toLowerCase() : value,
  )
  email: string;

  @ApiProperty({ example: 'Password@123' })
  @IsString()
  @IsStrongPassword(strongPasswordConfig)
  password: string;
}

export class SendVerifyMailDto {
  @ApiProperty({ example: 'example@danimai.com' })
  @IsEmail()
  @Transform(({ value }) =>
    typeof value === 'string' ? value.toLowerCase() : value,
  )
  email: string;
}

export class ResetPasswordDto {
  @ApiProperty({ example: 'Password@123' })
  @IsString()
  @IsStrongPassword(strongPasswordConfig)
  password: string;

  @ApiProperty({ example: 'vhsbdjsdfsd-dfmsdfjsd-sdfnsdk' })
  @IsString()
  reset_token: string;
}
Enter fullscreen mode Exit fullscreen mode
// src/utils/validation-options.ts

import {
  HttpException,
  HttpStatus,
  ValidationError,
  ValidationPipeOptions,
} from '@nestjs/common';

const validationOptions: ValidationPipeOptions = {
  transform: true,
  whitelist: true,
  errorHttpStatusCode: HttpStatus.UNPROCESSABLE_ENTITY,
  exceptionFactory: (errors: ValidationError[]) =>
    new HttpException(
      {
        status: HttpStatus.UNPROCESSABLE_ENTITY,
        errors: errors.reduce(
          (accumulator, currentValue) => ({
            ...accumulator,
            [currentValue.property]: Object.values(
              currentValue.constraints,
            ).join(', '),
          }),
          {},
        ),
      },
      HttpStatus.UNPROCESSABLE_ENTITY,
    ),
};

export default validationOptions;
Enter fullscreen mode Exit fullscreen mode
// src/utils/bootstrap.ts

import {
  INestApplication,
  ValidationPipe,
  VersioningType,
} from '@nestjs/common';
import { ConfigService } from '@nestjs/config';
import { DocumentBuilder, SwaggerModule } from '@nestjs/swagger';
import { SerializerInterceptor } from './serializer.interceptor';
import validationOptions from './validation-options';

// for swagger documentation builder it takes application and gives back us with setup
export const documentationBuilder = (
  app: INestApplication,
  configService: ConfigService,
) => {
  const config = new DocumentBuilder()
    .addBearerAuth()
    .setTitle(configService.get('app.name'))
    .setDescription('The Danimai API description')
    .setVersion('1')
    .build();

  const document = SwaggerModule.createDocument(app, config);
  SwaggerModule.setup('docs', app, document);
};

// some global implementation in one function like URI versioning, global pipe and interceptor
export const createApplication = (app: INestApplication) => {
  app.enableShutdownHooks();
  app.enableVersioning({
    type: VersioningType.URI,
  });
  app.useGlobalInterceptors(new SerializerInterceptor());
  app.useGlobalPipes(new ValidationPipe(validationOptions));

  return app;
};
Enter fullscreen mode Exit fullscreen mode
// src/utils/serializer.interceptor.ts

import {
  Injectable,
  NestInterceptor,
  ExecutionContext,
  CallHandler,
} from '@nestjs/common';
import { Observable } from 'rxjs';

@Injectable()
export class SerializerInterceptor implements NestInterceptor {
  intercept(context: ExecutionContext, next: CallHandler): Observable<unknown> {
    return next.handle().pipe();
  }
}
Enter fullscreen mode Exit fullscreen mode

Above are some new things we have introduced, like validation pipe that helps us in documentation. But be with me, we will understand them one by one as we move forward, just include them as for now. As we right now have a bootstrap file, we can utilize that in our main.ts file.

// src/main.ts

import { NestFactory } from '@nestjs/core';
import { AppModule } from './modules/app/app.module';
import { ConfigService } from '@nestjs/config';
import { createApplication, documentationBuilder } from './utils/bootstrap';

async function bootstrap() {
  const app = await NestFactory.create(AppModule);
  const configService = app.get(ConfigService);

  // bootstrapped functions
  createApplication(app);
  documentationBuilder(app, configService);

  await app.listen(configService.get('app.port') || 8000);
}
bootstrap();
Enter fullscreen mode Exit fullscreen mode

Even now we will not get any output as we don’t have included anything in auth.module.ts and that is not included in app.module.ts parent module file.

// src/modules/auth/auth.module.ts

import { Module } from '@nestjs/common';
import { EmailController } from './email.controller';
import { EmailService } from './email.service';

@Module({
  controllers: [EmailController],
  providers: [EmailService],
})
export class AuthModule {}
Enter fullscreen mode Exit fullscreen mode

And we include this module in main app module.

import { Module } from '@nestjs/common';
import { ConfigModule } from '@nestjs/config';
import { configLoads } from '../config';
import { TypeOrmModule } from '@nestjs/typeorm';
import { TypeORMConfigFactory } from '../database/typeorm.factory';
import { AuthModule } from '../auth/auth.module';

// here we have included that
const modules = [AuthModule];

export const global_modules = [
  ConfigModule.forRoot({
    load: configLoads,
    isGlobal: true,
    envFilePath: ['.env'],
  }),
  TypeOrmModule.forRootAsync({
    useClass: TypeORMConfigFactory,
  }),
];

@Module({
  imports: [...global_modules, ...modules],
})
export class AppModule {}
Enter fullscreen mode Exit fullscreen mode

It is just first setup to cover everything else, in upcoming series. If now you will run the application using

npm run start:dev
Enter fullscreen mode Exit fullscreen mode

It will show you this, where every router getting mapped with AuthModule Instance.

[10:23:03 PM] File change detected. Starting incremental compilation...

[10:23:03 PM] Found 0 errors. Watching for file changes.

[Nest] 29745  - 04/05/2024, 10:23:04 PM     LOG [NestFactory] Starting Nest application...
[Nest] 29745  - 04/05/2024, 10:23:04 PM     LOG [InstanceLoader] AppModule dependencies initialized +12ms
[Nest] 29745  - 04/05/2024, 10:23:04 PM     LOG [InstanceLoader] TypeOrmModule dependencies initialized +0ms
[Nest] 29745  - 04/05/2024, 10:23:04 PM     LOG [InstanceLoader] ConfigHostModule dependencies initialized +0ms
[Nest] 29745  - 04/05/2024, 10:23:04 PM     LOG [InstanceLoader] AuthModule dependencies initialized +5ms
[Nest] 29745  - 04/05/2024, 10:23:04 PM     LOG [InstanceLoader] ConfigModule dependencies initialized +0ms
[Nest] 29745  - 04/05/2024, 10:23:04 PM     LOG [InstanceLoader] TypeOrmCoreModule dependencies initialized +74ms
[Nest] 29745  - 04/05/2024, 10:23:04 PM     LOG [RoutesResolver] EmailController {/auth/email} (version: 1): +24ms
[Nest] 29745  - 04/05/2024, 10:23:04 PM     LOG [RouterExplorer] Mapped {/auth/email/register, POST} (version: 1) route +2ms
[Nest] 29745  - 04/05/2024, 10:23:04 PM     LOG [RouterExplorer] Mapped {/auth/email/verify, POST} (version: 1) route +0ms
[Nest] 29745  - 04/05/2024, 10:23:04 PM     LOG [RouterExplorer] Mapped {/auth/email/login, POST} (version: 1) route +1ms
[Nest] 29745  - 04/05/2024, 10:23:04 PM     LOG [RouterExplorer] Mapped {/auth/email/send-verify-email, POST} (version: 1) route +0ms
[Nest] 29745  - 04/05/2024, 10:23:04 PM     LOG [RouterExplorer] Mapped {/auth/email/reset-password-request, POST} (version: 1) route +0ms
[Nest] 29745  - 04/05/2024, 10:23:04 PM     LOG [NestApplication] Nest application successfully started +2ms
Enter fullscreen mode Exit fullscreen mode

And to view Swagger you have to go to http://localhost:8000/docs/ in browser, it will show the UI for swagger

Image description

But above is nothing as it does not work as expected, it only takes body validates and returns error if invalid or returns same data if valid. So we need to implement services for this controller.

Let us meet in the next, and complete this API endpoints one by one and deep dive into more concept of Nest JS. Thank you

Top comments (2)

Collapse
 
officialphaqwasi profile image
Isaac Klutse

Wow, great article

Collapse
 
depak379mandal profile image
depak379mandal

Thanks, Let me know if you have some insights or topic to discuss about in article.