In the last chapter, we saw what a sample route looks like in both vanilla Node and hapi, and how hapi is more configuration-oriented in its routing definition. In this chapter, I will expand on how hapi handles routing, making it easy to add routes in a scalable manner while being able to avoid making unnecessary mistakes. If you haven't got much experience with building web servers, this chapter will also be a good foundation in routing, covering the following topics:
By the end of this chapter, you will have the tools that you need to be able to create a JSON API, a static file server, and a fully functional website using a templating library. You will also be shown some patterns to simplify less trivial requests, so the control flow won't become a problem while using the hapi life cycle and route prerequisites. That may seem like a lot to cover in a single chapter, but you'll find it's actually not so complicated, especially with the tools hapi gives us. Let's begin now.
There are many ways to interpret a route in terms of web servers, but the easiest (or what I like to think of them as) one is the path that interacts with a particular resource on a web server. In most frameworks, including hapi, a route comprises three core properties:
In hapi, the handler looks like the following:
… server.route({ method: 'GET', path: '/', handler: function (request, reply) { return reply('Hello!'); } }); …
The preceding code should look familiar to the code sample of our server from the previous chapter. Again, like everything in hapi, we add a route by providing a configuration object with the required route properties. If any required properties are missing, the server will show an error on startup with a detailed error message, thus making it very easy to debug routing configuration issues.
The example in the previous section was quite trivial; let's now look at a more detailed one, and examine the different parts of a route in hapi:
… server.route({ method: 'GET', // [1] path: '/hello/{name}', // [2] config: { description: 'Return an object with hello message', // [3] validate: { // [4] params: { // [4] name: Joi.string().min(3).required() // [4] } // [4] }, // [4] pre: [], // [5] handler: function (request, reply) { // [6] const name = request.params.name; // [6] return reply({ message: `Hello ${name}` }); // [6] }, // [6] cache: { // [7] expiresIn: 3600000 // [7] } // [7] } }); …
You will notice that the handler has moved inside the config
object. When defining simple routes like our first one, this is not necessary. But as the route configuration becomes more complex, it allows us to simplify code by having the route controller logic in one object. This means that the logic could be defined somewhere else, like a different file, making for much easier management of code. This would be the 'controller' of the very common MVC pattern. With reference to the numbers in the preceding code example, let's now walk through our route configuration object:
This is an HTTP request
method defining how this route is to be accessed. It will generally be either GET
, POST
, PUT
, DELETE
, OPTIONS
, or PATCH
. A wildcard *
can be used, but I've found this only to be useful for a catch-all route to return a '404 not found' message. A point to note here is that for GET
and DELETE
, a payload will not be parsed or validated, as they should only be supplied with a POST
, PUT
, or PATCH
request.
This is the address of the resource that you are accessing. Routes are split into segments by the /
character, which can be accessed from request.params
inside the route handler. Segments here can be any of the following:
/sample/{segment1}/{segment2}
A request path must contain all segments to match this route.
?
): /path/{segment1?}/something
A request path must contain path
followed by something
, with an optional segment in between to match this route
*
): /path/{segments*}
A request must contain a path
segment, and then any number of segments from 0 upwards to match this route
*limit
): /path/{segments*2}
A request must contain a path
segment followed by up to two segments to match this route.
Do any of these routes conflict? Try to add all these examples, and see what happens.
The description adds no functionality to the hapi server, but is a form of documentation for your routes and can be used in documentation generation tools like Blipp available at https://github.com/danielb2/blipp and Hapi-Swagger available at https://github.com/glennjones/hapi-swagger.
Using joi, an excellent model validation library also developed by the Walmart team, you can add route-specific validation here. This is quite a large topic, and will be covered in Chapter 6, The joi of Reusable Validation, but it's good to be aware of it now.
Pre is the object key for route prerequisites. Route prerequisites are an array of functions that are executed before the handler is invoked. This gives you the chance to abstract and remove any common functionality that you might perform in the handlers as well as set up database connections or perform any checks you might need. So, your handler can focus on responding to the original request, making for much more readable code. Functions can easily be executed in series or in parallel, making control flow much simpler.
A sample route prerequisite is of the following form:
const routePrequesite = { method: pre1, assign: 'm1' };
Here pre1
is the function that will be invoked, and the optional assign key means that any value returned from the routePrerequesite
will be stored in the request
object, which will be accessible from:
request.pre.m1;
Let's look at what a full example would look like, noticing which prerequisites are executed sequentially and which in parallel (some route configuration code has been removed for brevity):
… config: { pre: [ setupDBConnection, // series checkUserExists, // series [ getUserDetails, getUserConnections ] // parallel ], handler: function (request, reply) { const user = request.pre.userDetails; user.connections = request.pre.userConnections; return reply(user); } } …
See how much we can simplify handlers when route prerequisites are used?
The handler is likely where most of the action will happen, and where most of your time will be spent when adding routes while developing your web servers. I generally try to separate my logic so that the handler can focus on what the route is supposed to do, and push everything else to prerequisites, making for quite simple handlers. I will go through the request
object and reply
interface in more detail in later sections within this chapter, but for now it's enough to know the following:
request
object contains all the data from the request, including the raw request along with the extra information provided in the earlier stages of the request life cycle.reply
interface returns whatever is provided to the user, and it will figure out the return type based on the data provided, such as JSON or PLAINTEXT. Also, one of the most common errors I see beginners make when starting out with hapi is calling reply()
twice in one handler. Always make sure to add a return statement with your reply, as it should always be the last thing you do in your handler.The preceding descriptions of each part of a route configuration should give you the tools you need to cover for building some simple APIs in hapi. Let's look at a couple of things that happen in the background when we create a route, such as hapi's pattern matching and route ordering works. Understanding what happens underneath takes away a lot of the 'magic' when your application does something unexpected, or when you try to do something different to a typical, normal use case. After that, we'll look at how to return different types of content from your web server, such as HTML files or templates.
3.141.28.107