The joy of validating with Joi
Tags: beginners, javascript, validation, joi
Published: 2022-09-23 03:58:13

Validation is a crucial step. But one look at the lines of IFs spawning from endless checks could send us over to NPM, hoping to find the perfect library.

And one of the validation libraries you would find is Joi. And like its name, it’s a joy to use.

With Joi, you can

> Describe your data using a simple, intuitive and readable language.

So to ensure some user input contains a name and a valid email, it’s simply

```js
const schema = Joi.object({
name: Joi.string()
   .min(3)
   .max(30)
   .required(),
email: Joi.string()
   .email({ minDomainSegments: 2, tlds: { allow: ['com', 'net'] } })
})
```

This code block validates an input to have a `name` property with a number of characters between 3 and 30, and an `email` with two domain parts (sample.com) and a top level domain (TLD) of either .com or .net.

But to get a better view of what Joi has to offer, let’s see how we could build a simple form that validates a user’s input according to a schema.

### A Simple Form Validation
Installing Joi is as easy as running:
```
npm i joi
```
After importing Joi at the top of your file with,
```js
const Joi = require("joi");
```
Joi can be used by first constructing a schema, then validating a value against the constructed schema.

For this example let’s assume that we already have four text fields taking in a user’s name and email and asks to enter a password twice.

![Image description](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/ybyoq3fk52vzll22mqya.png)
<figcaption align = "center">A simple form built with Material UI</figcaption>

Now to create the schema that Joi will validate against. Since a schema is designed to resemble the object we expect as an input, the schema for our four property form data object will look like this:

```js
  const objectSchema = {
    name: Joi.string().alphanum().min(3).max(30).required(),

    email: Joi.string().email({
      minDomainSegments: 2,
      tlds: { allow: ["com", "net"] },
    }),

    password: Joi.string()
      .pattern(new RegExp("^[a-zA-Z0-9]{3,30}$"))
      .required()
      .messages({
        "string.pattern.base": `Password should be between 3 to 30 characters and contain letters or numbers only`,
        "string.empty": `Password cannot be empty`,
        "any.required": `Password is required`,
      }),

    repeatPassword: Joi.valid(userData.password).messages({
      "any.only": "The two passwords do not match",
      "any.required": "Please re-enter the password",
    }),
  };
```

According to this schema:

- `name` is validated to be:
  - an alphanumeric string
  - between 3 to 30 characters
  - a required field
- `email` is checked to have :
  - two domain parts (sample.com)
  - a top level domain (TLD) of either .com or .net


### Custom Error Messages
The fields `name` and `email` use default error messages:


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

But the fields `password` and `repeatPassword` use `.messages()` to return a custom error message for a set of specific error types.

For example, the custom error messages for the `password` field are:
```js
.messages({
   “string.pattern.base”: `Password should be between 3 to 30 characters and contain letters or numbers only`,
   “string.empty”: `Password cannot be empty`,
   “any.required”: `Password is required`,
}),
```

The first one is a custom message for an error of type `string.pattern.base`, if the entered value does not match the RegExp string (since the `password` field is validated with a RegExp).

Likewise, if a an error of type `string.empty` is returned (the field is left blank) the custom error message “Password cannot be empty” is shown instead of the default.

Moving on to `repeatPassword`, `Joi.valid()` makes sure that the only valid value allowed for the `repeatPassword` field is whatever the user data is for the `password` field. The custom error message shown for an `any.only` error type is shown when the entered value does not match the provided allowed value, which is `userData.password` in this case.

The full list of possible errors in Joi can be viewed here:
https://github.com/hapijs/joi/blob/master/API.md#list-of-errors


### Validating the Form Field on an onChange event
In this example, each form field will have its own error message. So to make updating the state of each error message cleaner, an object was created to hold the values of error messages for all form fields with a useReducer hook to manage its state.

```js
//INITIAL VALUE OF ALL ERROR MESSAGES IN THE FORM
//Each property denotes an error message for each form field
const initialFormErrorState = {
   nameError: “”,
   emailError: “”,
   pwdError: “”,
   rpwdError: “”,
};
const reducer = (state, action) => {
   return {
      …state,
      [action.name]: action.value,
   };
};
const [state, dispatch] = useReducer(reducer,initialFormErrorState);
```

The `reducer` function returns an updated state object according to the action passed in, in this case the name of the error message passed in.

For a detailed explanation on the useReducer hook with an example to try out, feel free to check out my article on using the useReducer hook in forms.

{% embed https://dev.to/methmi/forms-with-react-hooks-ig0 %}

Moving on to handling the onChange events of the form fields, a function can be created to take in the entered value and the name of the error message property that should show the error message (to be used by the `dispatch` function of the useReducer hook).

```js
  const handleChange = (e, errorFieldName) => {
    setUserData((currentData) => {
      return {
        ...currentData,
        [e.target.id]: e.target.value,
      };
    });
    const propertySchema = Joi.object({
      [e.target.id]: objectSchema[e.target.id],
    });

    const result = propertySchema.validate({ [e.target.id]: e.target.value });
    result.error == null
      ? dispatch({
          name: errorFieldName,
          value: "",
        })
      : dispatch({
          name: errorFieldName,
          value: result.error.details[0].message,
        });
  };
```

Line 2 to line 7 updates the state of the `userData` object with the form field’s input. For simplicity, each form form field’s id is named its corresponding property on the `userData` object.

`propertySchema` on line 8 is the object that holds the schema of the form field that’s calling the `handleChange` function. The object `objectSchema` contained properties that were named after each form fields id, therefore, to call a fields respective schema and to convert it into a Joi object, `Joi.object({[e.target.id] :objectSchema[e.target.id],})` is used and the resulting schema object is stored in `propertySchema`.

Next, the input data is converted to an object and validated against the schema in `propertySchema` with `.validate()`.

This returns an object with a property called `error`, this property contains useful values like the error type (useful when creating custom messages) and the error message.

But, if the `error` property is not present in `result`, a validation error has not occurred, which is what we are checking in line 13.

If a `error` is present, the dispatch function is invoked with the name of the form error object’s field that should be updated in `name`, and the error message that it should be updated to in `value`.

This will make more sense when we look at how `handleChange` is called in a form field. Given below is how the form field ‘Name’ calls `handleChange`.

```js
<TextField
   //TextField component properties
   ...
   onChange={(value) => handleChange(value, “nameError”)}
   value={userData.name}
/>
```

`handleChange` accepts the value of the field as the first parameter and then the name of the respective error object’s field that the `dispatch` function in `handleChange` is supposed to update, `nameError`.

The object, `initialFormErrorState` had one property for each form field’s error message. In this case, any validation error in the ‘Name’ field will change the `nameError` property of `initialFormErrorState` which will in turn be displayed in the respective alert box under the form field.

Here’s a look at the finished form:

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

Hope this simple example helped show how joyful validation with Joi can be. 😊

_**Till next time, happy coding!**_

---
_Happy emoji vector created by freepik - www.freepik.com_