Validating hapi routes with joi

Now that we have learned about the importance of validation and the flexibility of joi, let's look at where we can apply it in our hapi applications. Fortunately, hapi provides first class support for validation on its route configuration objects. We saw this very briefly in Chapter 2, Adding Functionality by Routing Requests, both on a route configuration object and where the validation steps take place within the request life cycle.

On a route configuration object, we can provide validation rules through a validate object. With this, we can define specific validation rules for the request headers, parameters, query, payload, and also on the response. It might not be immediately obvious why we would validate our response, but we'll look at that further on in the chapter. Let's first look at an example of a route configuration with added validation rules.

Let's take our user store application from the previous chapters, and add some validation. If you remember the GET route, we asked the user to provide a string userId to retrieve a user, but in our handler, never actually verified that it existed. Let's fix this now by modifying this particular route configuration to add validation rules for ensuring that the userId is a string, it exists, and is of the right length. Let's see what that looks like now (with code removed for brevity):

…
server.route({
  method: 'GET',
  path: '/user/{userId}',
  config: {
    validate: {
      headers: true,
      params: {
        userId: Joi.string().min(4).max(40).required()
      },
      query: false
    },
    handler: function (request, reply) {
      // handler logic
    },
    description: 'Retrieve a user'
  }
});
…

So, as you can see, it's not too complicated to add validation to our routes in a very readable manner. Let's look more closely at the validate property that we added:

…
validate: {
  headers: true,
  params: {
    userId: Joi.string().min(4).max(40).required()
  },
  query: false
},
…

We've added three keys to our verification object, so let's go through each of those and examine what they do. Before explaining each, it's worth noting that for each key that we add to the validate object, the following values are supported:

  • true: This specifies that any value is allowed.
  • false: This specifies that no values are allowed.
  • A joi validation object: The provided joi validation object dictates what is allowed here.
  • A custom validation function: A function with the signature function(value, options, next) can be provided, which will dictate what values are allowed here. However, I've rarely seen this used in practice, as joi usually fulfills the need for any validation we may need to do; nevertheless, it is good to be aware that this option exists.

If validation fails on any of these properties, a Boom.badRequest() error will be returned to the client, and the request life cycle will never reach the handler.

Now let's go through each of the properties from the preceding example. First, we have specified validation for headers, indicating that any value is allowed by specifying true. If this is ever set to false for headers, it will cause all valid HTTP requests to fail. Adding headers: true is not required; by not adding a value for the headers property, it defaults to true. I have only added it here, so as to show that we can add validation rules to request headers.

For our params, we've supplied a joi object that specifies that one segment is allowed, which is the userId segment of our params. The rules applied are that the userId segment supplied in the request must be of minimum length 4 and maximum length 40.

By specifying query as false, we also specify that no query parameters are allowed or we reject the request. Note that we don't need to specify anything for the payload here, as a payload is never parsed for the GET routes. In fact, even trying to add validation for a HEAD or a GET route will throw an error as a reminder, just as trying to add a validation rule for a parameter that isn't defined in the route path would.

Let's now look at adding validation for POST route. Previously, we applied no validation, which meant that any type of object could be sent as a potential user object. Let's take our userSchema from the previous section, and apply that to the POST route (again, with code removed for brevity):

// Our Schemas
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');

// Route Configuration
server.route({
  method: 'POST',
  path: '/user',
  config: {
    validate: {
      params: false,
      query: false,
      payload: userSchema
    },
    handler: function (request, reply) {
      // handler logic
    },
    description: 'Create a user'
  }
});

Now with those small additions, we validate all our inputs to the user store in a readable manner. By using joi.object(), we have more options in how we'd like to handle our payload here as well. If we want to accept extra values outside of our defined rules, we can add .unknown() to our userSchema.

Now that we're comfortable validating our route inputs, let's look at how and why we would validate our route responses.

Validating route responses

I mentioned earlier that you can also validate the outputs of a route. This can be a particularly useful tool when building APIs that other services may reply upon, and when you want to ensure that any modifications to your handlers or models don't break an existing contract that you've specified.

We can do this by specifying a joi object as the schema that must be matched in our route configuration object. Of course, this validation step will add some performance overhead, so fortunately, we can also specify the percentage of responses we want validated. We can also specify a fail action, whether to send the response even though it doesn't match our expected schema and log it, or we can send a 500 error, indicating an internal server error event. Let's look at how we would configure all of the preceding options on the GET user route for our user store:

…
server.route(
  method: 'GET',
  path: '/user/{userId}',
  config: {
    validate: {
      headers: true,
      params: {
        userId: Joi.string().min(4).max(40).required()
      },
      query: false
    },
    handler: function (request, reply) {
      // handler logic
    },
    response: {
      schema: Joi.object().keys({
        id: Joi.string().min(4).max(40),
        details123: Joi.object()
      }),
      sample: 100,
      failAction: 'error'
    },
    description: 'Retrieve a user'
    }
  }
);
…

Here, we see the new response property added to our route configuration object. Even though this is a type of validation, it's configuration is specified via the response property. It's separated from the input validation so that the validate property in the route configuration is for input validation only. Let's quickly go through each option in the response configuration object in this example:

  • schema: This is the response payload validation rule. In the preceding example, it is a joi object, but like the validation properties for the inputs, it could be any of true, false, a joi object, or a custom validation function.
  • sample: The percentage of responses we want to validate. Validating our response payload provides a certain overhead, so we may not want to validate all our responses in a production environment. A good strategy here would be to validate all the responses in a development environment, and a much lower number, if any, in production.
  • failAction: The action we want to take when a response doesn't meet our validation rules. Our options are error or log. An error value here will mean that "500 Internal server error message" is sent to the client if the validation rules aren't met. A value of log here means that if the validation rules aren't met, the response will still be sent, but the response logged. A good strategy here would be to use error when in development and log when in production.

It's worth knowing that we can also pass an extra options object here, which will be passed to joi's validate function. Be conscious here though, that any options that require modification of the output will require the modify response validation option to be set to true. All the response validation options are covered in hapi's API documentation at http://hapijs.com/api#route-options.

With that, we should be comfortable with validating our response outputs. Let's look at one of my favorite features of hapi next: using all this route configuration to generate API documentation.

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

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