Chapter 2. Adding Functionality by Routing Requests

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:

  • Adding and configuring routes in hapi
  • The hapi routing algorithm
  • The hapi request life cycle
  • The hapi request object
  • The reply interface
  • Serving static files
  • Using templating engines to serve view

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.

Server routing

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:

  • the path through which you can access the route
  • a method by which you can access the path
  • a handler, which is a function to take an action based on the request and return the output to the user

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.

Route configuration

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:

Method

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.

Path

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:

  • Required: /sample/{segment1}/{segment2}

    A request path must contain all segments to match this route.

  • Optional (denoted by ?): /path/{segment1?}/something

    A request path must contain path followed by something, with an optional segment in between to match this route

  • Wildcard (denoted by *): /path/{segments*}

    A request must contain a path segment, and then any number of segments from 0 upwards to match this route

  • Limited wildcard (denoted by *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.

Description

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.

Validate

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

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?

Handler

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:

  • The 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.
  • The 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.

Cache

Here you can see cache headers for indicating to the browser how long this response may be cached for. This is different to the hapi application-level cache, which we will explore in later chapters.

Route configuration summary

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.

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

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