The joy of validating with Joi


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

const schema = Joi.object({
name: Joi.string()
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 ( 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,

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

A simple form built with Material UI

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:

  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}$"))
        "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 (
    • a top level domain (TLD) of either .com or .net

Custom Error Messages

The fields name and email use default error messages:

Image description

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:

   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:

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.

//Each property denotes an error message for each form field
const initialFormErrorState = {
   nameError: “”,
   emailError: “”,
   pwdError: “”,
   rpwdError: “”,
const reducer = (state, action) => {
   return {
      []: 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.

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).

  const handleChange = (e, errorFieldName) => {
    setUserData((currentData) => {
      return {
    const propertySchema = Joi.object({
      []: objectSchema[],

    const result = propertySchema.validate({ []: });
    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({[] :objectSchema[],}) 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.

   //TextField component properties
   onChange={(value) => handleChange(value, nameError)}

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

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

Till next time, happy coding!

Happy emoji vector created by freepik –

Leave a Reply

Your email address will not be published. Required fields are marked *

Previous Post

Tips: How to get last element of an array in javascipt

Next Post

palpatine received its first PR

Related Posts