Exposing and consuming plugins

In the previous section, we looked at how we can break up the business logic of our applications into smaller, more manageable chunks through plugins. We did this by attaching routes directly to the server object passed into the register, which is probably the simplest use case with plugins. But plugins won't always be used for just routing; you could perform logic on server start as with blipp, initialize database connections, create models for interacting with your data, and so on.

With developers still getting used to structuring server-side applications and the asynchronous nature of JavaScript, these types of use cases often are the beginning of messy or unstructured code due to the number of responsibilities placed on the developer. First of all, dependencies need to be acknowledged and dealt with in a sensible way—in the preceding examples, the database connection needs to be initialized before providing any model functionality. Secondly, we want a clean API where functionality is sandboxed in a consistent manner.

Fortunately, hapi provides APIs that support this with server.dependency() and server.expose(). Let's explore how to use these now.

Managing plugin dependencies

Let's make the previous example depend on another module requiring some form of persistent storage, for example, a database. Taking the plugin hello.js, let's modify it to add a dependency on a new plugin; we'll call database for this example:

exports.register = function (server, options, next) {
  server.dependency('database', (server, after) => {
    // Can do some dependency logic here.
    return after();
  });
  server.route({
    method: 'GET',
    path: '/hello',
    handler: function (request, reply) {
      return reply('Hello World
');
    }
  });
  return next();
};
exports.register.attributes = {
  name: 'hello'
};

In the preceding example, we see the new call to server.dependency(), which registers our dependency on the database plugin. Now when we try to start our server, it will fail with an error message making it clear that the database dependency wasn't respected. We would have to add this new database plugin before registering the hello plugin in our server.register(). This will also throw an exception if a circular dependency is detected, where a case of two plugins depending on each other may arise.

It is worth noting that multiple dependencies can be added here by passing an array of plugin names instead of one; this would look as follows:

…
server.dependency(['database', 'otherPlugin'], (after) => {
  return after();
});
…

There are multiple methods of registering plugin dependencies. If no dependency logic needs to be performed, there are two ways to synchronously register a plugin dependency. One is to use server.dependency() with no callback:

server.dependency(['database', 'otherPlugin']);

Or we can place the dependency in the attributes section of the plugin:

exports.register.attributes = {
  name: 'hello',
  dependencies: ['database', 'otherPlugin']
};

This is up to the preference of the developer; however, I usually opt for the first method, that is, via the server.dependency API. Often I find that I want to add logic after registering a dependency, so I prefer to keep all dependencies listed in one place in a consistent manner.

It is worth noting that for version management of plugins, npm peer dependencies (http://blog.nodejs.org/2013/02/07/peer-dependencies/) should be used, as hapi doesn't provide any native version management support.

Now that we have all the tools we need to nicely manage the dependencies of our plugins and business logic, let's look at the methods of combining them.

Exposing sandboxed functionality

Our plugin from the previous example is still quite trivial, as it only adds a route that returns a hard-coded string. Let's modify it so that it has some (very trivial!) functionality; we'll make it such that instead of just returning hello world, it returns a name if one is passed in:

exports.register = function (server, options, next) {
  const getHello = function (name) {
    const target = name || 'world';
    return `Hello ${target}`;
  };
  server.route({
    method: 'GET',
    path: '/hello/{name?}',
    handler: function (request, reply) {
      const message = getHello(request.params.name);
      return reply(message);
    }
  });
  next();
};

exports.register.attributes = {
  name: 'hello'
};

Now, as intended, we've added the ability to adjust the message to say hello to a name instead of world through the getHello() function. This is still pretty trivial, but what is not trivial is how you might use the function in another plugin or in the main server. One solution would be to make an npm module of the getHello() function that could be required in each plugin file, but this isn't always suitable. In this case, the function doesn't require any state and is quite simple, but in other cases it may use a database connection or depend on something else in the plugin. For these cases, you can use hapi's server.expose() function. With server.expose(), we can expose a plugin property so it is accessible through the following:

server.plugin['pluginName'].property;

Let's look at how we would expose the hello function now (with code removed for brevity):

…
const getHello = function(name) {
  const target = name || "world";
  return `Hello ${target}`;
};
server.expose({ getHello: getHello });
…

Now anywhere we have our server reference, we can use the getHello() function by calling:

server.plugins.hello.getHello('John'); // returns 'hello John'

This makes for a very clean and consistent API when sharing functionality from plugins. This is a very good example in which hapi provides the application infrastructure you need, so you can focus on building business logic, in a structured, clean, and testable manner.

Combining plugins

Let's make the preceding example a little less trivial to show that even with more complicated use cases, your code may remain readable and well structured. Instead of having just a route that responds with hello name, let's create a new plugin that lets us create and retrieve users from a persistent data store.

This means we'll need to add two routes, a POST and a GET route. They will have to interact with some persistent datastore, and finally expose the functionality so it can be used in other plugins. Sounds complicated? Let's see...

For this example, I will use LevelDB as the datastore. I use this as it's a simple in-process key-value data store that doesn't require any setup or configuration to start, and fortunately like with most databases or services, a plugin exists, making it very simple to use with hapi. If you're not familiar with LevelDB, I encourage you to check it out at http://leveldb.org/.

So let's begin with this example by creating our user management plugin that will add our routes and interact with LevelDB; we will put this in a file called user-store.js:

const Uuid = require('uuid');                           // [1]
const Boom = require('boom');                           // [2]
exports.register = function (server, options, next) {
  let store;                                            // [3]
  server.dependency('hapi-level', (server, after) => {  // [3]
    store = server.plugins['hapi-level'].db;            // [3]
    return after();                                     // [3]
  });                                                   // [3]
  const getUser = function (userId, callback) {         // [4]
    return store.get(userId, callback);                 // [4]
  };                                                    // [4]
  const createUser = function (userDetails, callback) { // [5]
    const userId = Uuid.v4();                           // [5]
    const user = {                                      // [5]
      id: userId,                                       // [5]
      details: userDetails                              // [5]
    };                                                  // [5]
    store.put(userId, user, (err) => {                  // [5]
      callback(err, user);                              // [5]
    });                                                 // [5]
  };                                                    // [5]
  server.route([
    {
      method: 'GET',                                    // [6]
      path: '/user/{userId}',                           // [6]
      config: {                                         // [6]
        handler: function (request, reply) {            // [6]
          const userId = request.params.userId;         // [6]
          getUser(userId, (err, user) => {              // [6]
            if(err) {                                   // [6]
              return reply(Boom.notFound(err));         // [6]
            }                                           // [6]
            return reply(user);                         // [6]
          });                                           // [6]
        },                                              // [6]
        description: 'Retrieve a user'                  // [6]
      },
      {
        method: 'POST',                                 // [7]
        path: '/user',                                  // [7]
        config: {                                       // [7]
          handler: function (request, reply) {          // [7]
            const userDetails = request.payload;        // [7]
            createUser(userDetails, (err, user) => {    // [7]
              if(err) {                                 // [7]
                return reply(Boom.badRequest(err));     // [7]
              }                                         // [7]
              return reply(user);                       // [7]
            });                                         // [7]
          },                                            // [7]
          description: 'Create a user'                  // [7]
        }                                               // [7]
  ]);
  server.expose({                                       // [8]
    getUser: getUser,                                   // [8]
    createUser: createUser                              // [8]
  });                                                   // [8]
  return next();
};
exports.register.attributes = {
  name: 'userStore'
};

Despite the increase in example complexity, and the introduction of some new concepts, I still find this code quite readable and intuitive; hopefully, you do too. Let's go through it in more detail. With reference to the numbers in the code comments, let's go through each section:

  • [1]: We are going to require the uuid npm module here so that we can create robust user IDs when creating users.
  • [2]: We will also use the boom module from the hapi ecosystem so that we can provide HTTP-friendly error objects.
  • [3]: Next we create a dependency on the hapi-level plugin. Inside the dependency callback, we assign our LevelDB reference to our store.
  • [4]: getUser is a function we'll create for retrieving users from our LevelDB datastore. Of course, we could do this inside the handler, but abstracting it out makes for more readable code, and we can then expose this functionality later.
  • [5]: Similar to getUser, we create a function for creating a user: createUser. In here, we use the uuid module to create a robust ID. Using the newly created ID and user details, we save our new user in LevelDB. We then pass any errors and the user object to our callback.
  • [6]: We add our route for retrieving a user. As we've moved most functionality to the getUser function, the handler code is quite simple. We just take the userId from the request parameters, and use that as the key to look up our user.
  • [7]: We add our route for creating a user. Again, this is very trivial as the heavy lifting is in the createUser function. We just pass in the request payload which contains all our user data to the createUser function, and reply with user created.
  • [8]: Finally, our plugin will expose these methods. So anywhere that we have the server reference, we can reuse them with server.plugins.userStore.getUser() and server.plugins.userStore.createUser(), making for a nice clean API.

So, not too complicated right? Let's add this plugin to our server now, and we can get our application up and running:

'use strict';
const Hapi = require('hapi');
const Blipp = require('blipp');
const HapiLevel = require('hapi-level');                [1]
const UserStore = require('./UserStore.js');            [2]
const server = new Hapi.Server();
server.connection({ port: 1337, host: '127.0.0.1' });
server.register([
  {                                                     [3]
    register: HapiLevel, options: {                     [3]
      config: { valueEncoding: 'json' }                 [3]
    }                                                   [3]
  },                                                    [3]
  UserStore,                                            [4]
  Blipp
], (err) => {
  server.start((err) => {
    console.log(`Server running at ${server.info.uri}`);
  });
});

Let's go through the changes to our index.js file in more detail here as well. With reference to the line numbers given with the preceding code, please find the explanation as follows:

  • [1]: We require hapi-level, a useful micro library to create a LevelDB datastore, which exposes the LevelDB API through server.plugins['hapi-level'].db, and stores the reference in the variable HapiLevel.
  • [2]: We require our new UserStore plugin.
  • [3]: We register HapiLevel, and pass it some configuration. We specify a value encoding of JSON, which means our datastore will now store and return JSON instead of the default UTF8 encoding.
  • [4]: Finally, we register our new UserStore plugin.

So, without too many changes to our server, we now have a fully functioning user store API that accepts and returns JSON, with a very easy to understand and manage codebase. When you start the server, it will log our routes with description thanks to blipp, so you should see the following:

Combining plugins

A great exercise here to increase your learning of hapi and of application structure in general, would be to try and add functionality to this server for updating and deleting users. I encourage you to try this now.

In this example, we've demonstrated some of the advantages that plugins provide us. In our applications, they are useful for breaking up application logic into reusable application building blocks. They also make for an easy-to-use way of integrating third-party libraries within your application, as we saw with hapi-level. When and where to use plugins is always an interesting debate, and there's no clear guideline given. It's up to the application developers like you. There is no clear best strategy, as every application is different.

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

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