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