Skip to main content
Version: 5.x

Composing enforce rules

When you have rules that you often use together or different groups of rules that describe the same behavior, you can compose them into a single rule for easier reuse.

compose allows us to create an "AND" relationship wrapper around multiple rules which acts like the regular enforce function.

A simple use-case example: Let's say we have multiple entities in our app that share some common characteristics, but some that are unique. We can compose the different validation rules for the common characteristics into a single rule that we can reuse across multiple entities.

Let's assume the following:

  • Some of the entities in our app have an id property.
  • The person entity has a name property that includes a first, middle and last name.
  • The user entity has both an id and a name property. It also has a username property.
  • Users can also have a friends property, which is an array of other users.

Expressing this with basic enforce rules is easy, but can be cumbersome, and also not very reusable.

import { enforce } from 'vest';
import 'vest/enforce/schema'; // for the schema rules

enforce(userObj).shape({
  id: enforce.number(),
  name: enforce.shape({
    first: enforce.string(),
    middle: enforce.optional(enforce.string()),
    last: enforce.string(),
  }),
  username: enforce.string(),
  friends: enforce.optional(
    enforce.arrayOf(
      enforce.shape({
        id: enforce.number(),
        username: enforce.string(),
        name: enforce.shape({
          first: enforce.string(),
          middle: enforce.optional(enforce.string()),
          last: enforce.string(),
        }),
      }),
    ),
  ),
});

Instead, we can compose these different characteristics into composites that can later on be further reused.

import compose from 'vest/enforce/compose';
import 'vest/enforce/schema'; // for the schema rules

const Entity = compose(
  enforce.loose({
    id: enforce.number(),
  }),
);

const Person = compose(
  enforce.loose({
    name: enforce.shape({
      first: enforce.string(),
      middle: enforce.optional(enforce.string()),
      last: enforce.string(),
    }),
  }),
);

const User = compose(
  Entity,
  Person,
  enforce.loose({
    username: enforce.string(),
    friends: enforce.optional(enforce.arrayOf(User)),
  }),
);

This way, each composite can be used individually, but can also be composed together to create a more complex rule that can be easily reused.

Using these composites is as easy as either calling the from within other compound rules, or calling them directly within a Vest test just like the regular enforce function:

User(userObj); // Throws an error when failing

Some notes

When composing rules, be mindful when you are composing rules that have a shape rule inside of them. If these shape extend one another, you should probably use loose so they allow for extended properties.