Simone P. M. 🇮🇹 Software Engineer

Elegant data validation with Vest

4 min read 1270

Elegant Data Validation With Vest

What could be more elegant than testing the validity of a form in the same way you’d test your code?

Vest is a powerful JavaScript data validation library that derives its syntax from modern JS frameworks such as Mocha or Jest. It is framework-agnostic, so you can drop it into your code in no time.

Unit testing applied to data validation? Yep!

If with Mocha we would do this:

import { assert } from 'chai'
let hello = () => 'Hello!'
assert.equal(hello, 'Hello!')

In a similar manner, with Vest we can do this:

import { enforce } from 'vest'
let hello = document.querySelector('#hello').value
enforce(hello).equals('Hello!')

“Smokin’!”

To begin with, we need to export a validation function — aka a suit — that will be used to perform tests on input data passed as an object.

import vest, { test, enforce } from 'vest'

const suitName = 'signUpSuit'

const validate = vest.create(suitName, formData => {

   test('username', 'Should not be empty', () => {
       enforce(formData.username).isNotEmpty()
   })

   test('password', 'Should be longer, () => {
       enforce(formData.password).longerThanOrEquals(8)
   })

   test('password_repeat', 'Should match password', () => {
       enforce(formData.password_repeat).equals(formData.password)
   })

})

export default validate

Now we can simply call it when the validation should occur in our logic:

import validate from './signup/validate.js'

let response = validate(formData)

Vest is a general-purpose data validator, so it does not include any form-serialization method — but hey, it is pretty easy to collect data!  Either you are writing vanilla JS:

let formData = inputs.reduce((data, input) => {
   return {
       ...data,
       ...{ [input.name]: input.value }
   }
, {})

Or using a framework like React:

const [formData, setFormData] = useState({})

const onInputChange = e => {
   setFormData(data => {
       ...data,
       ...{ [e.target.name]: e.target.value }
   })
}

Now that we know how to collect and pass our data, we need to handle the result of the validation suit in order to tailor a proper response for the user.

We made a custom demo for .
No really. Click here to check it out.

“Are we there yet?”

Validation always returns an object containing a pool of useful info and methods:

let response = validate(formData)

Anyway, code can quickly get messy when dealing with variables assigned on the fly. Vest provides a .done() method that can be chained multiple times to validation function. Here we can pass a callback function in order to handle the response gracefully:

validate(formData).done(callback)

Specifying an optional key before the callback function, we can explicitly wait for the validation of a single field. The callback will be executed only when the condition is met, despite its order in the chain.

validate(formData)
   .done('username', usernameCallback)
   .done(formCallback)

“We need a response team!”

To check whether any test has failed, we need to call the .hasErrors() response method.

if (response.hasErrors()) let errors = response.getErrors()

If true, the full list of errors will be accessible from the .getErrors() method as a key-value object. For every field that didn’t pass a test, there will be an array containing a list of error messages.

Multiple tests can be provided for a single field, and the order of appearance will be reflected in the array.

test('username', 'Cannot be empty', () => {
   enforce(formData.username).isNotEmpty()
})

test('username', 'Must be longer than 3 chars', () => {
   enforce(formData.username).isLongerOrEquals(4)
})

If a name is passed to the methods above, only errors for that particular field will be returned:

if (response.hasErrors('password')) {
   let passwordErrorMessage = response.getErrors('password')[0]
   // ...

When vest.warn() is called for a test, its messages will be accessible from the .hasWarnings() and .getWarnings() methods instead.

This allows us to separate response messages between two different levels of importance.

test('username', 'Should not be a number', () => {
   vest.warn()
   enforce(formData.username).isNotNumeric()
})

“Next group, please.”

Tests can be grouped in order to filter which inputs should be tested at a certain point of our logic. This is highly useful — for example , to validate multipage forms sending data in sequential steps.

const validate = vest.create(suitName, formData => {
   group('formPage1', () => {
       // test(...)
   })
})

The validation result by group will be accessible from .hasErrorsByGroup() and .getErrorsByGroup():

if (response.hasErrorsByGroup('formPage1')) {
   // ...

Tests can also be filtered out using vest.skip(). This method accepts either a field name or a group name and can be used to avoid a test under certain conditions.

const validate = vest.create('purchaseSuit', formData => {
   if (!formData.coupon) vest.skip('coupon')
   // ...

“I do real-time.”

By calling vest.only(), we can ask Vest to execute tests for a particular field or group only.

const validate = vest.create('userSuit',
   (data = {}, inputName = '') => {
       if (inputName) vest.only(inputName)
       // ...

This is particularly useful when validating data in real time while the user is typing:

const onInputChange = e => {
   validate({ [e.target.name]: e.target.value }, e.target.name)
       .done(e.target.name, callback)
}

Vest is stateful by default, so even if we are passing data for a single field — as in the example above — the response will return the most recent validation results for all the fields that have already been tested, making it much easier to update our application state accordingly.

Furthermore, the latest suit state is always accessible from vest.get(suitName), and can be reset via vest.reset(suitName).

If you would like to run stateless, import a validate() function directly from Vest instead of creating a new one. This is better suited for server-side validation, where the response is expected to be idempotent. As such, use:

const response = validate('stateless', data => {
   // ...
})

Rather than:

const validate = vest.create('stateful', data => {
   // ...
})

“We need servers for that!”

Vest allows for async validation. This is particularly useful when we need to check data against entries in a database — like checking whether a username is already in use.

test('username', 'Should not already exist', async () => {
   return await checkIfUserExists(formData.username)
})

Keep in mind that async responses could come after other sync tests already finished running. Generally speaking, it’s suggested to execute async tests at the bottom end of the suit logic, after all sync requests have run.

In such cases, we can preserve server resources using the vest.draft() method. This way, we can perform conditional operations against the state of a suit that is currently running and avoid unneeded requests:

test('username', 'Cannot be empty', () => {
   enforce(formData.username).isNotEmpty()
})
if (!vest.draft().hasErrors('username')) {
  test('username', 'Should not already exist', async () => {
       return await doesUserExist(formData.username)
   })
}

“We have a rule.”

enforce() rules can be chained together, and if the defaults do not satisfy your needs, you can simply create custom logics returning a Boolean or — for better reusability — extend the default pool of methods:

import { enforce } from 'vest'

const customRules = {
   isTruthyStr: value =>
       ['true','on'].includes(String(value).toLowerCase()),
   isFalsyStr: value =>
       ['false','off'].includes(String(value).toLowerCase()),
}

enforce.extend(customRules)

With Vest’s any(), we can express a logical OR between rules. If any of the enforced rules are satisfied, the whole test is considered as passed:

test('comment', 'Must be at least 140 chars, if provided.',
   any(
       () => enforce(data.comment).isEmpty(),
       () => enforce(data.comment).longerThanOrEquals(140)
   )
)

“Class, class, class.”

A tailor-made suit is nothing without class, right? Vest provides a classNames() function, a powerful method for associating CSS classes to DOM elements based on the validation state:

import classNames from 'vest/classNames'
const getFieldClass = classNames(response, {
  untested: 'is-untested',
  tested: 'is-tested',
  valid: 'is-valid',
  invalid: 'has-errors',
  warning: 'has-warnings',
})

This allows for live updates to our components’ appearance:

function InputText(props) {
   return <input
       type='text'
       name={props.name}
       value={props.value}
       className={getFieldClass(props.name)}
       onChange={e => onInputChange(e)}
    />
}

In this way, it is possible to express complex UI logics based on inputs’ state, in real time, without getting spaghetti!

You come here a lot! We hope you enjoy the LogRocket blog. Could you fill out a survey about what you want us to write about?

    Which of these topics are you most interested in?
    ReactVueAngularNew frameworks
    Do you spend a lot of time reproducing errors in your apps?
    YesNo
    Which, if any, do you think would help you reproduce errors more effectively?
    A solution to see exactly what a user did to trigger an errorProactive monitoring which automatically surfaces issuesHaving a support team triage issues more efficiently
    Thanks! Interested to hear how LogRocket can improve your bug fixing processes? Leave your email:

    Are you adding new JS libraries to improve performance or build new features? What if they’re doing the opposite?

    There’s no doubt that frontends are getting more complex. As you add new JavaScript libraries and other dependencies to your app, you’ll need more visibility to ensure your users don’t run into unknown issues.

    LogRocket is a frontend application monitoring solution that lets you replay JavaScript errors as if they happened in your own browser so you can react to bugs more effectively.

    https://logrocket.com/signup/

    LogRocket works perfectly with any app, regardless of framework, and has plugins to log additional context from Redux, Vuex, and @ngrx/store. Instead of guessing why problems happen, you can aggregate and report on what state your application was in when an issue occurred. LogRocket also monitors your app’s performance, reporting metrics like client CPU load, client memory usage, and more.

    Build confidently — .

    Simone P. M. 🇮🇹 Software Engineer

    Leave a Reply