# 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. 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:

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 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/src/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:

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

# 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) in the data section of your Vue component:

data: () => ({
  form: {
    email: '',
    name: '',
    password: '',
    password_repeat: '',
  },
  apiErrors: {
    email: [],
    name: [],
    password: [],
    password_repeat: [],
  },
}),

Then, for each field in your form, you'll want to add some convenience computed functions:

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

const validator = new UserValidator(SIGNUP_FORM_FIELDS);

module.exports = {
  computed: {
    emailErrorText: fieldErrorText('email', validator),
  },
};

fieldErrorText is a helper function (located in wood/ui/src/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).

If you have defined your form and apiErrors variables differently in your component, you can still use this helper, by providing those names as parameters:

addressErrorText: fieldErrorText('address', billingValidator, 'billingForm', 'billingApiErrors'),

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:

computed: {
  /**
   * Form is valid if all inputs are full and valid.
   *
   * @return {Boolean}
   */
  formValid() {
    return validator.valid(this.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.

# Adding a new validator

To add a new validator with some sample code to get you rolling, run:

nodewood add validator FEATURE NAME

Replace FEATURE with the name of the feature you wish to add a validator for, and NAME with the name of the validator you wish to create.