DEV Community

Hasan Zohdy
Hasan Zohdy

Posted on

28-Nodejs Course 2023: Validation Part VI: Custom Validation

We have made such an amazing progress in validation, but there is still more to be done.

Configurations Types

We have created configurations list successfully, but let's give it a more fancy touch, let's define a type for it.

Create types.ts file in src/core/validator directory.

// src/core/validator/types.ts

export type ValidationConfigurations = {
  /**
   * Whether to stop validator after first failed rule
   *
   * @default true
   */
  stopOnFirstFailure?: boolean;
}
Enter fullscreen mode Exit fullscreen mode

We added only one property for now to see how i'm writing it.

We first write documentation for it, if we set a default value then we write it in the documentation, and then we define the type.

Now let's add the rest of the configurations.

// src/core/validator/types.ts

import Rule from "./rules/rule";

export type ValidationConfigurations = {
  /**
   * Whether to stop validator after first failed rule
   *
   * @default true
   */
  stopOnFirstFailure?: boolean;
  /**
   * Return Error Strategy
   * If strategy is `first` then it will return a single error in string from the rule errors list
   * If strategy is `all` then it will return an array of string that contains all errors.
   *
   * The `all` strategy will be affected as well with `stopOnFirstFailure` if it is set to `true`
   * and strategy is set to `all` it will always return an array with one value
   *
   * @default first
   */
  returnErrorStrategy?: "first" | "all";
  /**
   * Response status code
   *
   * @default 400
   */
  responseStatus?: number;
  /**
   * Validation keys
   */
  keys?: {
    /**
     * Response error key that will wrap the entire errors
     *
     * @default errors
     */
    response?: string;
    /**
     * Input key name
     *
     * @default input
     */
    inputKey?: string;
    /**
     * Single Error key (when strategy is set to first)
     *
     * @default error
     */
    inputError?: string;
    /**
     * Multiple Errors key (when strategy is set to all)
     *
     * @default errors
     */
    inputErrors?: string;
  };
  /**
   * Rules list that will be used in the validation process
   */
  rules?: Record<string, typeof Rule>;
};
Enter fullscreen mode Exit fullscreen mode

All types are pretty much self explained, except the rules one, we used the Record type which means we're telling Typescript the rules will be an object, its key will be string and its value will be typeof Rule class.

Now let's export it in the validator index then import it in the validator.ts configuration file.

// src/core/validator/index.ts
export { default as RequiredRule } from "./rules/required";
export { default as Rule } from "./rules/rule";
export { default as StringRule } from "./rules/string";
export * from "./types";
export { default as Validator } from "./validator";
Enter fullscreen mode Exit fullscreen mode
// src/config/validation.ts
import {
  RequiredRule,
  StringRule,
  ValidationConfigurations,
} from "core/validator";

const validationConfigurations: ValidationConfigurations = {
  stopOnFirstFailure: true,
  returnErrorStrategy: "first",
  responseStatus: 400,
  rules: {
    [RequiredRule.ruleName]: RequiredRule,
    [StringRule.ruleName]: StringRule,
  },
  keys: {
    response: "messages",
    inputKey: "key",
    inputError: "error",
    inputErrors: "errors",
  },
};

export default validationConfigurations;
Enter fullscreen mode Exit fullscreen mode

The amazing thing about typescript, is that now you can use the auto complete feature in your vscode.

Try to remove all configurations and inside the object press ctrl + space and you will see all the configurations that you can use.

Missing Rule Handler

As we're going big, we need to make sure that our code is working as expected, what if we tried to add a rule that does not exist in the rules list?

In that case the application will definitely crash, so let's add a handler for that.

// src/core/validator/rules-list.ts
import chalk from "chalk";

  /**
   * Validate the rules
   */
  public async validate() {
    for (const ruleName of this.rules) {
      const RuleClass = config.get(`validation.rules.${ruleName}`);

      if (!RuleClass) {
        throw new Error(
          chalk.bold(
            `Missing Rule: ${chalk.redBright(
              ruleName,
            )} rule is not listed in ${chalk.cyan(
              "validation.rules",
            )} configurations,`,
          ),
        );
      }

      const rule = new RuleClass(this.input, this.value);

      await rule.validate();

      if (rule.fails()) {
        this.errorsList.push(rule.error());

        if (config.get("validation.stopOnFirstFailure", true)) {
          break;
        }
      }
    }
  }
Enter fullscreen mode Exit fullscreen mode

We added a check here to see if the rule exists in the rules list, if not then we throw an error.

But we need to catch that error to be displayed as you won't see any error in the console.

Open the request class to wrap the validator in a try catch block.

// src/core/http/request.ts

  /**
   * Execute the request
   */
  public async execute() {
    if (this.handler.validation) {
      const validator = new Validator(this, this.handler.validation.rules);

      try {
        await validator.scan(); // start scanning the rules
      } catch (error) {
        // this is needed to catch the error thrown by the missing rule handler
        console.log(error);
      }

      if (validator.fails()) {
        const responseErrorsKey = config.get(
          "validation.keys.response",
          "errors",
        );
        const responseStatus = config.get("validation.responseStatus", 400);

        return this.response.status(responseStatus).send({
          [responseErrorsKey]: validator.errors(),
        });
      }
    }

    return await this.handler(this, this.response);
  }
Enter fullscreen mode Exit fullscreen mode

Now go to create-user and add email rule that does not exist in the validation.rules list.

Mark stopOnFirstFailure as false in the validation.ts file to see the difference.

You should now see something like this in your terminal:

Error

Custom Validation

So we're so far now good with validation, but sometimes rules are not enough, we might need to do some custom validations on the requests besides the rules.

That's where we'll introduce a new validate method in the handler validation object.

Validate Method

The handler (controller) has a validation property, which contains the rules property to validate inputs.

We will also add the ability to add a validate method to the validation object so we can do any custom validation.

How it works

The validate method will receive the request and the response as parameters, and it will be executed after the rules validation.

If the validate method returns a value, this will be returned, otherwise the handler will be executed.

Also we'll make it async function so we can perform any async operation inside it.

Let's jump into the code.

// src/core/http/request.ts

  /**
   * Execute the request
   */
  public async execute() {
    if (this.handler.validation) {
      // rules validation
      if (this.handler.validation.rules) {
        const validator = new Validator(this, this.handler.validation.rules);

        try {
          await validator.scan(); // start scanning the rules
        } catch (error) {
          console.log(error);
        }

        if (validator.fails()) {
          const responseErrorsKey = config.get(
            "validation.keys.response",
            "errors",
          );

          const responseStatus = config.get("validation.responseStatus", 400);

          return this.response.status(responseStatus).send({
            [responseErrorsKey]: validator.errors(),
          });
        }
      }

      // custom validation
      if (this.handler.validation.validate) {
        const result = await this.handler.validation.validate(
          this,
          this.response,
        );

        if (result) {
          return result;
        }
      }
    }

    return await this.handler(this, this.response);
  }
Enter fullscreen mode Exit fullscreen mode

We wrapped the rules validation with a new if statement, to see if the request has rules property.

If the rules property exists, then execute and validate it first, if failed, then stop executing the handler and the custom validator as well.

Now let's give it a try, open create-user handler and add the validate method.

// src/app/users/controllers/create-user.ts
import { Request } from "core/http/request";

export default async function createUser(request: Request) {
  const { name, email } = request.body;

  return {
    name,
  };
}

createUser.validation = {
  // rules: {
  //   name: ["required", "string"],
  //   email: ["required", "string"],
  // },
  validate: async () => {
    return {
      error: "Bad Request",
    };
  },
};
Enter fullscreen mode Exit fullscreen mode

We commented the rules only for testing purposes, and added a validate method that returns an object.

If the validate returns any value, then it will return that value, otherwise it will execute the handler.

Conclusion

We've reached the end of this article, we've added the configurations types, we also wrapped the rules validation in a try catch block to catch any errors thrown by the rules.

Also we added a custom validation method to the handler, so we can do any custom validation on the request.

🎨 Project Repository

You can find the latest updates of this project on Github

😍 Join our community

Join our community on Discord to get help and support (Node Js 2023 Channel).

🎞️ Video Course (Arabic Voice)

If you want to learn this course in video format, you can find it on Youtube, the course is in Arabic language.

💰 Bonus Content 💰

You may have a look at these articles, it will definitely boost your knowledge and productivity.

General Topics

Packages & Libraries

React Js Packages

Courses (Articles)

Oldest comments (0)