Introduction to joi

As mentioned earlier, joi (https://github.com/hapijs/joi) is an object schema validation library used in nearly every module throughout the hapi ecosystem. If you tried adding an incorrect configuration object to a connection, a route configuration object, or when registering plugins, and found that the server threw a detailed validation error, that was joi at work. When the hapi team were going for a configuration-over-code approach for building a framework, having an excellent object schema validator to validate all the configuration objects and provide detailed errors was important. The same goes for when building an application.

Similar to testing, validation is one of those things that developers might not give the full effort to in projects, as the repercussions aren't immediately obvious. If it's not made easy, it might not be done properly. Fortunately, joi has such an easy-to-use fluent API, that using method chaining, which I'll show soon, makes it very easy to write validation schemas. This makes for readable schemas that act as good documentation for your code. It's also very flexible in the way you can define schemas, with support for using nested schemas for more complex use cases. Let's look at what a simple schema looks like. Let's create a very simple schema to validate a potential username:

const usernameSchema = Joi.string().min(4).max(40);

Hopefully, it's pretty easy to gather what the schema validates. Given an input, it checks for a string that has a minimum length of 4 characters and maximum length of 40 characters.

We can then test joi schemas against their inputs using joi.assert() or joi.validate(). The difference between the two is that joi.assert() returns the result or throws an error when it is called, whereas joi.validate() always returns an object of the form:

{ error: … , value: … }

Here error will be null if the validation is successful, and value will be the resulting object. An optional synchronous callback can be used for dealing with the output of the validation call as well. Let's now try and test our usernameSchema parameter with some test cases, and examine the outputs:

Joi.validate('john', usernameSchema);
// output:
//  { error: null, value: 'john' }

Joi.validate('jo', usernameSchema);
// output: 
//  {   error:
//    { [ValidationError: "value" length must be at least 4 characters long]
//      name: 'ValidationError',
//      details: [ [Object] ],
//      _object: 'jo',
//      annotate: [Function] },
//      value: 'jo' }

This demonstrates how to validate some simple values pretty easily. A common error I see new users encountering is allowing empty strings. The initial thought might be to just set joi.string().min(0), but this actually won't allow an empty string, as they are not allowed by default. To allow an empty string, we use the following function:

Joi.string().allow('');

An empty string is a falsey value in JavaScript, so it is better to be secure by default here when allowing empty strings. By using .allow(''), you acknowledge that you're allowing potential falsey values. This is a common source of bugs that I notice when something like the following is used:

// check is username is defined
if (user.username) {
  // perform some action if username is defined
}

Here the username could be defined, but with a falsey value, which means the if statement evaluates to false. This is something to be conscious of when allowing empty strings or working with other potential falsey values.

The preceding example is still quite trivial, and could easily be replicated with a single if statement. Let's now look at a more complex or real-world example where we want to validate a full user object and its properties instead of a simple string.

Given joi's fluent API, to create a schema for an object, we will use joi.object() for the schema instead of joi.string(). To test the properties within an object, we then use joi.object().keys({}). Let's create a user schema now that checks that a given input is an object, with a username property within:

const userSchema = Joi.object.keys({
  username: Joi.string().min(4).max(40)
});

Again, not too complicated and very readable. We also defined our schema for our username earlier, so we can just reuse that here:

const userSchema = Joi.object.keys({
  username: usernameSchema
});

This is an example of reusing and nesting schemas. You can nest complete objects within objects, which makes for quite detailed and complex schemas in joi. You can also modify existing schemas through method chaining. Let's demonstrate this by making it such that the username property on our user object is required. This is achieved by adding .required() to our existing usernameSchema:

const userSchema = Joi.object.keys({
  username: usernameSchema.required()
});

Real world cases of these schemas require more logic than just checking whether a key exists or an object property is of a certain type. Often, we will need to rename keys, make it such that two keys can't exist at the same time, or ensure that a minimum number of keys are provided. The good news is that joi supports all these use cases, and I recommend reading its API documentation extensively. Let's add more validations to our userSchema object to demonstrate some of these cases.

When thinking about a potential user schema for an application, usually what we want to record is an identifier such as an e-mail or a username. We'd also want to record a password and then store some extra meta information about our users. Given this, let's add some extra validation rules to our userSchema to allow the email, username, password, and meta keys. Let's also assume that we only want one way to verify a user, either by their email or by their username, so they can't have both. We also want to verify that the e-mail provided is, in fact, a valid e-mail, that the password provided is a string of minimum length 3 and maximum length 30 using only alphanumeric characters, and finally, that our meta will be an object that can store any keys.

Note

Please note, it is bad security practice to put a maximum length on passwords, or limiting the content to alphanumeric characters. Here, the validation rules we have added are to highlight the flexibility of joi only. If this were a production system, I would likely have only a minimum password length validation rule, and a much higher maximum—in the region of 1024, if at all.

In joi, we could represent this by the following schema:

const usernameSchema = Joi.string().min(4).max(40);
const userSchema = Joi.object().keys({
  username: usernameSchema.required(),
  email: Joi.string().email(),
  password: Joi.string().regex(/^[a-zA-Z0-9]{3,30}$/),
  meta: Joi.object()
}).xor('username', 'email');

Not too complicated and still very readable. If I haven't convinced you on the importance of joi yet, I recommend you to try replicating the preceding userSchema with if statements now, keeping in mind that you have to provide a consistent and detailed error for each validation step broken—not so easy, right?

If you test this with joi.validate(), by default, it aborts all the validation steps once it hits the first validation error. To get all the errors, you would just add the optional third options parameter to the call to joi.validate(), which will look like the following:

Joi.validate(user, userSchema, { abortEarly: false });

Now that we have a good grounding in joi, let's look at how and where we would use joi in our hapi applications.

..................Content has been hidden....................

You can't read the all page of ebook, please click here login for view all page.
Reset
18.220.139.168