# Validators

Before submitting forms to your API (for user convenience), and before accepting API input (for security), you will want to validate form input. Since this happens on both the front-end and back-end, Nodewood makes use of shared Validator helpers to define the validation rules and error messages for your model fields.

# Creating a new Validator

From the root of your project, run nodewood add:validator FEATURE NAME. (e.g.: nodewood add:validator scheduling calendar.) This will create a new Validator in app/features/FEATURE/lib/validators/NAMEValidator.js. Typically, you will pair Validators with Models, so if you have a PostModel, you'll likely want to pair it with a PostValidator.

# Adding Rules to Validators

Nodewood's Validator classes allow you to add Rules in the constructor, which make it very easy to take advantage of validator.js (opens new window):

constructor(fields) {
  super(fields);

  this.rules = {
    name: [
      new Rule('isEmpty', { code: ERROR_EMPTY, title: 'You must enter a name.' }),
    ],
  };
}

In the above case, whenever validating the name field, it will be checked to see if it is empty. If so, it will add an error object with the code and title provided to the errors returned.

The above rule triggers when the isEmpty validator returns true about the name field, but we can also trigger when a validator returns false:

email: [
  new Rule('isEmpty', { code: ERROR_EMPTY, title: 'You must enter a name.' }),
  new NotRule('isEmail', { code: ERROR_INVALID_EMAIL, title: 'You must enter a valid email address.' }),
],

Rules are evaluated in order, and the first rule to trigger skips the rest. In this case, if the email field is not empty, it is checked to see if it is a valid email address. A NotRule will trigger if the validator fails, so in this case, the second rule will trigger when the email field is not a valid email address.

You can use any of validator.js's Validators (opens new window) as the first parameter of the rule. If it requires additional arguments, those can be passed as the third parameter to the Rule's constructor:

password: [
  new NotRule(
    'isLength',
    {
      code: ERROR_MIN_LENGTH,
      title: `Password must be at least ${PASSWORD_LENGTH} characters.`,
      meta: { minLength: PASSWORD_LENGTH },
    },
    [{ min: PASSWORD_LENGTH }],
  ),
],

Additional validator rules have been added for convenience:

Name Description
equalsField(str, field) Compare the value of this field to the value in another field. Check UserValidator for an example.

You can also extend the Rule class from #lib/Rules to add your own validator rules. Any function defined on that class can be used as a validator rule. For an example, see the equalsField function defined in wood/lib/Rules.

# Using Validators in the API

Since a single validator can be used to validate multiple forms, you'll need to pass an array of the fields you wish to validate to the Validator constructor.

The base Controller class that your controllers will inherit from has a validate() function that accepts a form object containing your data to validate and a validator object, which accepts an initialized Validator:

  const SIGNUP_FORM_FIELDS = ['email', 'password', 'passwordRepeat'];

  async function signup(req, res) {
    this.validate(req.body, new UserValidator(SIGNUP_FORM_FIELDS));
    // Signup code goes here
  }

Any errors found during validation are thrown as a Standard400Error, which is handled in an Express middleware and returned as a response with a 400 Status Code and a body that describes the error using the JSON API error format (opens new window):

{
  "errors": [
    {
      "code": "ERROR_UNIQUE",
      "source": {
        "parameter": "name",
      },
      "title": "You already have a project with that name.",
    },
  ],
}

# Using multiple validators in the API

If your form is a composite form (i.e. the fields will be used to update multiple models/tables, for intance in the case of the team signup page), you may want to use multiple Validators to take care of all of the fields in the form. In this case, you can actually pass multiple Validators to the validate() function:

  async function signup(req, res) {
    this.validate(req.body, [
      new UserValidator(USER_FORM_FIELDS),
      new TeamValidator(TEAM_FORM_FIELDS, { aliases: { team_name: 'name' } }),
    ]);
    // ...
  }

For more information on what aliases are and how to use them, refer to Aliasing fields.

# Using Validators in the UI

Using Validators in the UI is a little more involved, but it's still a fairly simple pattern. First, you need to add an object for your form data and errors returned from the API (since only the API will be able to validate things like duplicate fields, failed 3rd-party transactions). Then, for each field in your form, you'll want to add some convenience computed functions:

import { computed, ref } from 'vue';
import { UserValidator, SIGNUP_FORM_FIELDS } from '#features/users/lib/validators/UserValidator';
import { fieldErrorText } from '#ui/lib/forms';

const validator = new UserValidator(SIGNUP_FORM_FIELDS);

const form = { name: ref(''), email: ref('') };
const apiErrors = { name: ref([]), email: ref([]) };

const nameErrorText = computed(fieldErrorText('name', validator, form, apiErrors));
const emailErrorText = computed(fieldErrorText('email', validator, form, apiErrors));

fieldErrorText is a helper function (located in wood/ui/lib/forms.js) that will attempt to validate the provided form field with the provided validator and combine it with any apiErrors that are defined. If the form field is empty, no errors are displayed (since a form might require a field to not be empty to submit, but you don't want to display that error passive-aggressively in all fields when it loads).

Now in your template, you can use these computed properties to alter and display your form data:

<input v-model="form.email">
<div v-if="emailErrorText.length >= 0" class="text-red-600">
  {{ emailErrorText }}
</div>

You can also create a convenience computed property for the entire form:

const formValid = computed(() => validator.valid(form));

Then you can use it to enable/disable the submit button:

<button :disabled="! formValid">
  Submit
</button>

This prevents you from having to duplicate the critical code: the actual validation logic. That way, if your validation logic ever changes, you can change the rules in just the Validator, and know that your API and UI will both adapt without you needing to make any changes there.

# Aliasing fields

From time to time, you'll need to have form fields that have a different name from what you'll use in the rules key in your validators, for example, when combining validators to validate a single form.

An example of this is SignupNoCCTeamPage.vue, where the UserValidator and TeamValidator are combined to validate the user fields and team name fields of one combined form. In this case, we can't use name for the team name, since it's already being used for the user name, so instead we have to alias this entry.

This is done by providing an optional parameter when creating the validator:

const teamValidator = new TeamValidator(FORM_FIELDS, { aliases: { team_name: 'name' } });

This means that later, when you check the form for errors with:

teamValidator.errors(this.form, this.apiErrors);

It will use team_name to pull values from the form, but compare them against the rules for name in the validator. This will effectively and safely decouple the form field names from the validator rule names, when necessary.

Note, in the returned errors array, the key for the aliased field will be the alias. For example, if you are aliasing name as team_name, as above, the error key for that field will be team_name, not name. This is because, usually, you are aliasing fields because you already have other fields with that key from other validators that you want to include. If you just merged the objects, the latter one with the same key would overwrite it. With the aliases preserved, it is safe to merge the objects without worrying about losing error information.