DEV Community

Jayvee Ramos
Jayvee Ramos

Posted on

5. Adding Validation and System Logging

Introduction:

In this tutorial, we will explore how to add validation and system logging to our NestJS application. Adding validation to user input is considered best practice as it ensures that user input is reliable and secure.
Additionally, we will enhance our system logging using the NestJS Pino library, which makes logging requests and responses a breeze. By the end of this tutorial, you'll have a better understanding of how to validate user input and improve your application's logging capabilities.

Step 1: Installing Dependencies

To begin, we need to install two essential dependencies: class-validator and class-transformer. These libraries will help us validate user input effectively. You can install them using npm:

npm install class-validator class-transformer
Enter fullscreen mode Exit fullscreen mode

Step 2: Enhancing Logging with NestJS Piano

We'll improve our system logging by using the NestJS Pino library, which automates request and response logging. It also allows us to correlate logs effortlessly without additional configuration. Here's how to set it up:

2.1. Install NestJS Pino:

npm install nestjs-pino pino-http
Enter fullscreen mode Exit fullscreen mode

Step 3: Implementation of validation and logger

To add validation and logger to our app, it's very easy.
All we have to do is call useGlobalPipes and useLogger here on our application.
to do this just open your apps/reservations/src/main.ts
and update the code with the following content:

import { NestFactory } from '@nestjs/core';
import { ReservationsModule } from './reservations/reservations.module';
import { ValidationPipe } from '@nestjs/common';
import { Logger } from 'nestjs-pino';

async function bootstrap() {
  const app = await NestFactory.create(ReservationsModule);
  app.useGlobalPipes(new ValidationPipe());
  app.useLogger(app.get(Logger));
  await app.listen(3000);
}
bootstrap();
Enter fullscreen mode Exit fullscreen mode

Format Logs with Pino Pretty:
To make the logs more readable, install the pinot-pretty package:

npm install pino-pretty
Enter fullscreen mode Exit fullscreen mode

Step 4: Abstracting Logging for Reusability

To ensure that we can reuse our logging setup in other microservices and maintain a clean structure, we'll create a logger module in our common library.

Run the Nest CLI command to generate a logger module in your common project:

nest generate module logger
Enter fullscreen mode Exit fullscreen mode

it will give you this promp choose common

? Which project would you like to generate to?
  reservations [ Default ]
> common
Enter fullscreen mode Exit fullscreen mode

it will create a logger module inside your libs/common/src/logger/logger.module.ts

populate it with the following code:

import { Module } from '@nestjs/common';
import { LoggerModule as PinoLogerModule } from 'nestjs-pino';

@Module({
  imports: [
    PinoLogerModule.forRoot({
      pinoHttp: {
        transport: {
          target: 'pino-pretty',
          options: {
            singleLine: true,
          },
        },
      },
    }),
  ],
})
export class LoggerModule {}
Enter fullscreen mode Exit fullscreen mode

Code Explanation
Import Statements:
import { Module } from '@nestjs/common';: This line imports the Module decorator from the '@nestjs/common' package, which is used for defining modules in NestJS.

import { LoggerModule as PinoLogerModule } from 'nestjs-pino';: This line imports the 'LoggerModule' from the 'nestjs-pino' package, renaming it as 'PinoLoggerModule' to be used in the current code.

Module Definition:
@Module({ ... }): This decorator is used to define a NestJS module. The configuration object inside the decorator contains the module's settings.

Module Configuration:
imports: Inside the module configuration, the imports property is specified as an array.

PinoLoggerModule.forRoot({ ... }): This is an import of the 'PinoLoggerModule' using the .forRoot() static method provided by the module.

pinoHttp: This is an object passed as a configuration parameter to the 'PinoLoggerModule.forRoot()' method. It configures the behavior of the 'pino' logger used in this module.
transport: This nested object defines the transport options for the logger.

target: The 'target' property specifies the target output for the logger. In this case, it's set to 'pino-pretty', indicating that the logger will output logs in a pretty-printed format.

options: This object provides additional options for the logger's transport.

singleLine: true: The 'singleLine' option is set to 'true', which means that each log message will be formatted as a single line, making it more readable and concise.

Export Logger Module:
Create an index.ts file in your logger module and export everything from the logger module.
to do this just run the following code on your terminal:

touch libs/common/src/logger/index.ts
Enter fullscreen mode Exit fullscreen mode

then populate it with the following code:
export * from './logger.module';

make sure to also export the logger module in our libs/common/src/index.ts

update your index code with the following content:

export * from './config';
export * from './database';
export * from './logger';
Enter fullscreen mode Exit fullscreen mode

Import Logger Module:
In your reservations module, import the logger module from the common library and use it in your application.
to do this open your
apps/reservations/src/reservations/reservations.module.ts
and update with the following content

import { Module } from '@nestjs/common';
import { ReservationsService } from './reservations.service';
import { ReservationsController } from './reservations.controller';
import { DatabaseModule, LoggerModule } from '@app/common';
import { ReservationsRepository } from './reservations.repository';
import {
  ReservationDocument,
  ReservationSchema,
} from './entities/reservation.entity';

@Module({
  imports: [
    DatabaseModule,
    DatabaseModule.forFeature([
      { name: ReservationDocument.name, schema: ReservationSchema },
    ]),
    LoggerModule,
  ],
  controllers: [ReservationsController],
  providers: [ReservationsService, ReservationsRepository],
})
export class ReservationsModule {}

Enter fullscreen mode Exit fullscreen mode

What we did was simply import the LoggerModule to use it in our reservation application.

Test our Logger Setup
run npm run start:dev
if you saw this beautiful logger in your terminal

[11:17:30.769] INFO (12092): ReservationsController {/reservations}: {"context":"RoutesResolver"}
[11:17:30.774] INFO (12092): Mapped {/reservations, POST} route {"context":"RouterExplorer"}
[11:17:30.775] INFO (12092): Mapped {/reservations, GET} route {"context":"RouterExplorer"}
[11:17:30.775] INFO (12092): Mapped {/reservations/:id, GET} route {"context":"RouterExplorer"}
[11:17:30.776] INFO (12092): Mapped {/reservations/:id, PATCH} route {"context":"RouterExplorer"}
[11:17:30.776] INFO (12092): Mapped {/reservations/:id, DELETE} route {"context":"RouterExplorer"}
[11:17:30.777] INFO (12092): Nest application successfully started {"context":"NestApplication"}
Enter fullscreen mode Exit fullscreen mode

it means that our setup was successful

Step 5: Adding Validation to Reservations

Now that we have improved our logging setup, let's focus on adding validation to our reservations, specifically for the create reservation request.

5.1. Use Class Validator Decorators:
In your Create Reservation DTO, use Class Validator decorators to validate the incoming data.

open your apps/reservations/src/reservations/dto/create-reservation.dto.ts and update the content with the following code:

import { Type } from 'class-transformer';
import { IsDate, IsNotEmpty, IsString } from 'class-validator';

export class CreateReservationDto {
  @IsDate()
  @Type(() => Date)
  startDate: Date;

  @IsDate()
  @Type(() => Date)
  endDate: Date;

  @IsString()
  @IsNotEmpty()
  placeId: string;

  @IsString()
  @IsNotEmpty()
  invoiceId: string;
}

Enter fullscreen mode Exit fullscreen mode

Class Properties Explanation:

startDate: Date;: This property represents the start date of the reservation. It is annotated with the @IsDate() decorator, which indicates that the value of this property should be a valid JavaScript Date object.

endDate: Date;: This property represents the end date of the reservation and, like startDate, is annotated with the @IsDate() decorator to ensure it is a valid JavaScript Date object.

placeId: string;: This property represents the place or location identifier for the reservation. It is annotated with the @IsString() decorator to ensure that it is a string, and with @IsNotEmpty() to ensure that it is not an empty string.

invoiceId: string;: This property represents the invoice identifier for the reservation. It is also annotated with @IsString() to ensure that it is a string, and with @IsNotEmpty() to ensure that it is not an empty string.

the @Type decorator from Class Transformer to specify that certain properties should be returned as date objects.

5.2. Enable Whitelisting:
To ensure that only properties defined in the DTO are allowed, set the whitelist option to true in your validation pipe configuration in the main.ts file.
in your **apps/reservations/src/main.ts** update your
app.useGlobalPipes(new ValidationPipe()); with the following code specifying the whitelist to true

  app.useGlobalPipes(
    new ValidationPipe({
      whitelist: true,
    }),
  );
Enter fullscreen mode Exit fullscreen mode

With validation in place, your API will reject requests that don't meet the defined criteria, making it more robust and secure.

Test our validation setup

Scenario: Creating a Reservation

1. User Action: A user wants to create a new reservation in your system. They prepare a JSON request to send to your API.

2. Request Body: The user sends a POST request to the /reservations endpoint with a JSON request body, which includes various reservation details. For testing validation, let's consider the following cases:

a. Valid Request:

{
    "startDate": "11/26/2023",
    "endDate":"11/30/2023" ,
    "placeId":"223443" ,
    "invoiceId":"0098748" 
}

Enter fullscreen mode Exit fullscreen mode

b. Invalid Request (Missing Required Field):

{
  "startDate": "2023-11-01",
  "endDate": "2023-11-05"
}

Enter fullscreen mode Exit fullscreen mode

c. Invalid Request (Invalid Date Format):

{  
"startDate": "2023-11-01222",
  "endDate": "2023-11-05",
  "placeId": "12345",
  "invoiceId": "67890"
  }

Enter fullscreen mode Exit fullscreen mode

d. Invalid Request (Empty String for Required Field):

{
  "startDate": "2023-11-01",
  "endDate": "2023-11-05",
  "placeId": "",
  "invoiceId": "67890"
}

Enter fullscreen mode Exit fullscreen mode

3. Expected Response:

a. In the case of a valid request, you should receive a 201 Created response with the created reservation details.

b. For an invalid request due to missing a required field, you should receive a 400 Bad Request response with a message indicating the missing field.

c. If the request contains an invalid date format, you should also receive a 400 Bad Request response with a message indicating the date format is incorrect.

d. If the request includes an empty string for a required field, you should again receive a 400 Bad Request response.

4. Testing Steps:

  • Use a tool like Postman or a tool for making HTTP requests.
  • Send each of the mentioned request scenarios to your API.
  • Verify that the responses match the expected results.

By testing these scenarios, you can ensure that your validation rules are correctly applied to incoming requests, and your NestJS application responds accordingly to both valid and invalid input.

Conclusion:

In this tutorial, we've learned how to add validation to a NestJS application, enhance system logging, and abstract the logging setup for reusability. We've also explored how to use Class Validator and Class Transformer to validate and transform data effectively. By following these steps, you can improve the reliability and security of your NestJS application. In the next tutorial, we will Dockerize our app and explore adding authentication to our backend.

Top comments (0)