When building a robust and scalable API, it’s important to implement proper validation within your application not only in terms of security but also to keep your database clean. If you’re working with robust backend APIs, NestJS can be a great tool to use, and, you can add object validation with NestJS and Joi to make your application secure.
This article will show you how to implement Joi object validation in NestJS via a basic CRUD example.
Heavily inspired by Angular, NestJS is an especially popular framework for building scalable and robust applications. It provides a clean architecture based on modules, controllers, and providers to help you get started. It supports TypeScript, but you can also write your applications with vanilla JavaScript.
NestJS doesn’t impose any programming paradigm on you. You are free to use object-oriented, functional, or functional reactive programming. It also provides an excellent routing mechanism under the hood and natively supports the HTTP server framework Express. You can also configure your NestJS application to use Fastify.
NestJS has three main building blocks: modules, controllers, and providers.
@Module
decorator to provide metadata. NestJS uses these files to organize the application structureIn recent years, Node.js has become a popular programming language, and, because developers want to write production-ready APIs faster, the demand for a quality backend framework has also increased. Enter NestJS, which helps developers write efficient codes faster.
Let’s discuss the benefits of using NestJS.
All developers know that it’s vital to validate data coming from clients. If you have ever used MongoDB and mongoose in Node.js, you are likely familiar with mongoose schemas. Mongoose schemas help describe the data and easily add validators for the data. Joi is very similar to schemas.
Joi is a widely used Node.js data validation library that provides a simple, intuitive, and readable API to describe data. It’s primarily used to validate data sent from API endpoints and allows you to create blueprints of the data type you want to accept.
Here is a simple example of describing schema with Joi:
const schema = Joi.object().keys({ name: Joi.string().alphanum().min(3).max(30).required(), birthyear: Joi.number().integer().min(1970).max(2013), });
NestJS provides many options to scaffold code according to your needs, such as the CRUD recipe. This allows you to scaffold a CRUD with endpoints within a few seconds from the Nest CLI.
To install the Nest CLI on your computer, run the following command:
npm i -g @nestjs/cli
The next step is to generate a Nest application. The Nest CLI uses the following command:
nest new project-name
Here, project-name
is the name of the project. After the command completes, run the following command to scaffold the CRUD endpoints:
nest g resource users
It’ll ask you a few questions, such as which transport layer to use. Once you choose the options according to your preference, Nest will scaffold the CRUD API. For example, an API with the users
endpoint will be generated from the above command. You can see the new users
folder.
Now, if you run the application with npm run start:dev
, you’ll see the endpoints logged in the console. Your server will be started at port 3000
.
You can either check the endpoints by visiting them or opening the users.controllers.ts
file. This file contains the routes for the CRUD API. The services for each API are defined in the users.service.ts
file, and all these files are under the users
folder.
If you look at the GET
method for finding a single item in the users.controllers.ts
file, you’ll find that there is no validation set up. You can use anything as an ID, and Nest will not throw a validation error.
The OWASP top ten list of security risks mentions that injection attack is still one of the most popular security risks. OWASP also mentions that an application is vulnerable to injection attacks when “user-supplied data is not validated, filtered, or sanitized by the application.”
This clearly shows that data validation is an important security concern to keep in mind when building applications. There are built-in pipes that can verify or modify the input. NestJS has eight built-in pipes. If you want the ID to be only of integer type, you can use the ParseIntPipe
pipe. Here’s an example:
@Get(':id') findOne(@Param('id', ParseIntPipe) id: string) { return this.usersService.findOne(+id); }
If you try to hit the endpoint with any ID other than a numeric value, you’ll receive the following error.
Using a inbuilt pipe is simple, but using it for a large schema is complicated. Joi makes it easier to design schemas and implement validation in NestJS. Let’s implement Joi for the NestJS project.
Generally, any script that can be injected into NestJS is the Pipe
class. Pipes primarily have two use cases:
You can read more about pipes in the official documentation.
The first step is to install the necessary packages. Here, only the Joi package is required. Run the following command to install the package.
npm i joi
Now, create a new file called validation.pipe.ts
inside the users
directory. Creating a custom pipe to implement validation is pretty straightforward. Here’s a code snippet to help you understand.
import { PipeTransform, BadRequestException, ArgumentMetadata } from '@nestjs/common'; export class ValidationPipe implements PipeTransform { transform(value: any, metadata: ArgumentMetadata) { return value; } }
Any schema passed into this pipe constructor will be checked for the configured Joi validation schema. To make the above validator work, open the create-user.dto.ts
file inside the dto
folder.
Here, define a schema type that the API will use when saving the data. For simplicity, assume that the schema sent by the user and held by the database have the same structure.
Let’s assume the API takes firstname
, lastname
, email
, isVerified
, and phoneNumber
as input. The DTO will look like this:
export class CreateUserDto { public firstname: string; public lastname: string; public isVerified: boolean; public email: string; public phoneNumber: number; }
Now, define the Joi schema inside the user.dto.js
file. You can also use separate files to store the schemas. The Joi user schema is simple for this example.
import Joi from 'joi'; export const UserSchema = Joi.object({ firstname: Joi.string().required(), lastname: Joi.string().required(), email: Joi.string().email().required(), isVerified: Joi.boolean().required(), phoneNumber: Joi.number(), }).options({ abortEarly: false, });
The schema is pretty self-explanatory. The string()
method ensures that the input is of type string
, and the required()
method makes certain the fields are inside the input. Similarly, boolean
and number
make sure the types are boolean or number.
The options
method takes other options inside an object. The abortEarly
method, when set to true
, stops the validation when it finds the first error. Otherwise, it returns all the errors.
Now that the schemas are ready, it is time to update the validation pipe accordingly.
Here is the complete validation.pipe.ts
file.
import { PipeTransform, BadRequestException } from '@nestjs/common'; import { CreateUserDto } from './dto/create-user.dto'; import { UserSchema } from './dto/user.dto'; export class CreateUserValidatorPipe implements PipeTransform<CreateUserDto> { public transform(value: CreateUserDto): CreateUserDto { const result = UserSchema.validate(value); if (result.error) { const errorMessages = result.error.details.map((d) => d.message).join(); throw new BadRequestException(errorMessages); } return value; } }
The custom validator class accepts and returns the CreateUserDto
class. The const result = UserSchema.validate(value);
validates the result according to the defined Joi schema. If the result has any errors, the results are mapped using the map
method. The error messages are joined together. Finally, the error messages are sent to the client. Otherwise, it returns the input value.
If the input passes the validation, it’ll show the message, “This action adds a new user
,” according to the method defined inside the user.service.ts
file.
We’ve now implemented Joi into NestJS. You can see if the validation is working by sending the JSON payload to the endpoint http://localhost:3000/users
.
Now that we have seen some prior validation types, let’s explore some more types. You’ll see validating dates, arrays, and strings with Joi and NestJS.
Validating the date with Joi is also straightforward. The date()
validator checks if the passed data is of type date
.
Under the hood, the date validator uses the JavaScript Date.parse
function to convert a few invalid dates to valid dates. For example, if the user passes 2/31/2029
, it will be converted to 3/3/2029
.
The date validator has a few methods like greater
, less
, and iso
. The greater
function can check if a date is greater than the specified date. Similarly, the less
function will check if the value is less than the given date.
Let’s make a validation field for the date of birth. In the user.dto.ts
file, add the following line below phoneNumber
:
dob: Joi.date().less('1-12-2022')
Here, the validator will check if the date is less than 1 December 2022. If you try to pass a date greater than the given, it’ll return an error.
The date
validator also has an iso
method that checks if the passed date is of valid ISO 8601 format. You can learn more about the date function from the official documentation.
Joi with NestJS also provides validators for arrays. There are multiple methods available with the array
validator. For example, you can use the length
function with the array
validator to check if the array is of the specified length. The schema would be:
arr: Joi.array().length(5)
The above object will only accept an array that is of length 5
. Using the max
and min
function, you can also set a minimum or a maximum number of acceptable array elements.
You can also validate an array of objects using the array
validator. Let’s look at the validator object to understand how it can be done.
items: Joi.array().has( Joi.object({ name: Joi.string().valid('item1', 'item2', 'item3').required(), price: Joi.number().required(), }), ),
Here, the item
object will hold an array of objects. The object can have two fields: name
and price
. The name
field can only contain values item1
, item2
, or item3
. The valid
method holds the valid values for the specific field. And the price
field can only be of numeric type.
The request for items must be similar to the following. Otherwise, it’ll throw an error:
"items": [ { "name": "item1", "price": 4 }, { "name": "item1", "price": 5 } ]
There are many other functions available on the array
validator. You can check all of the functions here.
Joi also contains multiple functions for validating different types of strings. You cannot pass an empty string by default, but this rule can be overwritten using allow('')
method. You can also set up default values using the default()
method.
Multiple methods are available on strings, like alphanum
, base64
, creditCard
, ip
, etc. The creditCard
validator method uses the Luhn Algorithm for validating credit card numbers. The usage is similar to the above methods. You simply chain the methods with the validator function. Here’s an example:
creditCard: Joi.string().creditCard().required(),
The above-discussed data types are only a few that Joi offers. Many other types of data can be easily validated using Joi and NestJS. You can refer to the official documentation to expand your validation schema.
Thunder Client is a VS Code extension used for quick API testing. It is similar to Postman or Insomnia, but it is lightweight and has fewer functionalities than Postman.
Thunder Client is enough for testing because the API built in this application only consists of essential features. You’ll find Thunder Client very similar if you are familiar with Postman or any other REST client.
By default, it contains two panels. The left panel specifies the request, type, data, headers, contents, etc. The right column shows the status of the request, the response, and other response-related information.
The API route for this application is localhost:3000/users
. You can hit the route with GET
or POST
methods to check if it’s working. But before hitting the routes, make sure your application is running. You can run the application by running the following command in the terminal:
npm run start:dev
Now, on the left side panel of Thunder Client, set the method as post
and change the default URL to http://localhost:3000/users
. Hit Send, and the right panel will show the result shown below.
Now, let’s try to send a POST
request to the same endpoint. The POST
request should contain a JSON body. The JSON body should include the following fields:
firstname
lastname
email
isVerified
phoneNumber
dob
creditCard
items
Let’s take a look at a request example:
{ "firstname": "Subha", "lastname": "Chanda", "email": "[email protected]", "isVerified": true, "phoneNumber": 7777777777, "dob": "1-1-2022", "creditCard": "4242424242424242", "items": [ { "name": "item1", "price": 4 }, { "name": "item1", "price": 5 } ] }
The above JSON request is valid. Try pasting this request to the JSON Content field in the left panel of Thunder Client. Also, change the request type to POST. Send the request, and you’ll receive the output as shown below:
The right panel would show the text This action adds a new user, and the JSON body will be logged to the console.
Let’s change the credit card data to invalid data. Removing three digits from the credit card will make it invalid. If you send the request after removing three numbers, you’ll receive the error stating "\"creditCard\" must be a credit card"
.
Similar to this, you can test the other data types. A JSON object will be returned for invalid data, and the message key in the object will consist of the error.
This article provided an overview of NestJS and Joi and the importance of validation in our apps, then walked you through implementing validation within a NestJS application. I hope you found it useful.
Remember: it is essential to implement proper validation methods to build a robust application. You can check out the Joi and NestJS documentation to better understand the library and frameworks.
Install LogRocket via npm or script tag. LogRocket.init()
must be called client-side, not
server-side
$ npm i --save logrocket // Code: import LogRocket from 'logrocket'; LogRocket.init('app/id');
// Add to your HTML: <script src="https://cdn.lr-ingest.com/LogRocket.min.js"></script> <script>window.LogRocket && window.LogRocket.init('app/id');</script>
Hey there, want to help make our blog better?
Join LogRocket’s Content Advisory Board. You’ll help inform the type of content we create and get access to exclusive meetups, social accreditation, and swag.
Sign up nowLearn how to manage memory leaks in Rust, avoid unsafe behavior, and use tools like weak references to ensure efficient programs.
Bypass anti-bot measures in Node.js with curl-impersonate. Learn how it mimics browsers to overcome bot detection for web scraping.
Handle frontend data discrepancies with eventual consistency using WebSockets, Docker Compose, and practical code examples.
Efficient initializing is crucial to smooth-running websites. One way to optimize that process is through lazy initialization in Rust 1.80.