Chapter 7. Making Your Application Production Ready

So, we're already at the last chapter. I hope you have enjoyed learning about hapi and how to build applications with it so far. In this last chapter, I'd like to cover some of the remaining common tasks that we often encounter when building applications, and getting them ready for production.

Here, you'll learn about the following:

  • Using databases to store our data
  • Server methods and adding caching to our applications through them
  • Logging and the importance of logs for applications in production
  • Different tools to add to your tool belt for debugging Node applications as well as some hapi-specific tools from the hapi ecosystem
  • Some advice on production infrastructure
  • Places you can go to in order to continue your learning about hapi

Let's begin.

Persisting data

I covered persisting data earlier in this book in our user store example using an in-process database called LevelDB. While LevelDB works quite well for demo purposes, due to a very simple installation and setup, it has a very limited feature set, and isn't recognized widely as a production-ready database.

Most applications today use some of the better known and tested databases, such as MongoDB (https://www.mongodb.org/), PostgreSQL (http://www.postgresql.org/), or MySQL (https://www.mysql.com/). While this is because of the wider feature sets that they offer, it is also, as it has been proven in production environments, something of vital importance when it comes to a database. When an application crashes or runs slowly, you'll find you have some frustrated customers, but losing their data usually means you've lost them for good!

Taking this into account, I wanted to add an example to demonstrate integrating one of these databases with hapi to show that this can be done just as easily as we did with our LevelDB example.

Fortunately, for all of the aforementioned databases, there are hapi plugins developed by the community to handle most of the setup of integrating a database with hapi. These, when registered, usually handle the initial connection setup, and will make a database connection available as a property on the request object for our route handlers, or via the server.plugins['somedatabase'] reference.

The database we'll use for this example is MongoDB, as it is one of the most popular databases as a part of the Node-based technology stack at the moment. MongoDB is a NoSQL document-oriented database if you are not familiar with it. This means that instead of the traditional SQL structured databases such as MySQL or PostgreSQL where data is stored in tables and rows, MongoDB stores data as JSON documents as part of a collection.

NoSQL databases have grown exponentially with the popularity of Node. This is likely down to the fact that they have a smaller learning curve, and data is stored in a format that is easy to interact with from JavaScript. Moreover, they are generally schema-less and meaning less need for big migrations or downtime.

MongoDB

Let's see what is involved in converting our user store example to using MongoDB instead of LevelDB as our database. You'll first need to install MongoDB if you have not done so already. Details on installation for each operating system are provided on the MongoDB website at https://docs.mongodb.org/manual/.

While MongoDB has an official driver for Node (https://www.npmjs.com/package/mongodb), you'll likely not need to use it directly very often. In Node, most frameworks will have a node module to aid integrating into your application, and hapi here is no different. Here we'll use the hapi-mongodb package (https://www.npmjs.com/package/hapi-mongodb), which will take care of all the initial setup for integrating a MongoDB connection into our application.

Let's now update our user store example. First we'll start by modifying our application entry point, the index.js file. To use MongoDB instead of LevelDB here actually only requires two changes; I've highlighted both with comments:

'use strict';
const Hapi = require('hapi');
const Blipp = require('blipp');
const HapiMongo = require('hapi-mongodb');             // [1]
const UserStore = require('./user-store.js');
const HapiSwagger = require('hapi-swagger');
const Inert = require('inert');
const Vision = require('vision');
const server = new Hapi.Server();
server.connection({ port: 1337, host: '127.0.0.1' });
server.register([
  { register: HapiMongo, options: { url: 'mongodb://localhost:27017/user-store' } },            // [2]
  UserStore,
  Blipp,
  Inert,
  Vision,
  HapiSwagger
], (err) => {
  if (err) {
    throw err;
  }
  server.start((err) => {
    if (err) {
      throw err;
    }
    console.log(`Server running at ${server.info.uri}`);
  });
});

Let's just explain those comments before moving forward:

  • [1]: We require the hapi-mongodb module here instead of where we previously required the hapi-level module
  • [2]: We now register the hapi-mongodb module as a plugin in our server

However, if you try run this now (with MongoDB running), you'll find that we get an error:

Error: Plugin user-store missing dependency hapi-level in connection: http://127.0.0.1:1337

Can you hazard a guess as to why we get this error? Remember, we declared a dependency on the hapi-level plugin back in our original user-store example? Well, this is no longer going to be met; we'll have to update our user-store.js file to account for this. We only catch this by handling the error passed to us from the server.start() method.

I'd like to highlight the importance of error handling here on the plugin registration also. Without this, our server would start even if MongoDB wasn't running. It's worth testing this; try stopping your MongoDB instance from running and then launching this example again. You should get the following error:

Error: connect ECONNREFUSED 127.0.0.1:27017

The lesson to be learned here is that you should always handle errors from the start. It'll make you far more productive down the line when things are failing unexpectedly, with no errors being thrown or logged.

Now that we're more comfortable with our error handling, let's finish off updating this example to use MongoDB. There's little more work required to update our user-store.js file to use MongoDB than in the index.js file. We have to first modify our dependency, then update the logic of how we add and retrieve users to match the MongoDB driver APIs instead of those of LevelDB.

Let's now look at what changes need be made to our user-store.js file. As the file itself is quite long, I will just go through the parts that need to be updated, starting with the changes in registering server.dependency():

…
let store;
let ObjectID;
server.dependency('hapi-mongodb, (_server, after) => {
  store = _server.plugins['hapi-mongodb'].db.collection('users');
  ObjectID = _server.plugins['hapi-mongodb'].ObjectID;
  return after();
});
…

Here we register the variables for store and ObjectID to use throughout the plugin, which will be updated within the server.dependency() callback. You'll notice that I used _server here as the variable name. This is not to conflict with the variable server in the outer scope.

The ObjectID variable used here is a new concept used in MongoDB, so let me just explain it before moving forward. In LevelDB, we generated our unique identifiers for users via the uuid module. MongoDB has its own means of generating IDs, via ObjectID, so we will use that here.

Finally, we update the dependency to be on the hapi-mongodb plugin as opposed to the hapi-level plugin.

Let's now look at the createUser and getUser functions, as their logic will change slightly to use the MongoDB APIs instead of LevelDBs. Let's look first at the createUser function:

…
const createUser = function (userDetails, callback) {
  const userId = new ObjectID();
  const user = {
    _id: userId,
    details: userDetails
  };
  store.insertOne(user, (err, result) => {
    if (err) {
      callback(Boom.internal(err));
    }
    getUser(userId, callback);
  });
};
…

Here we see the call to ObjectID() to create our user ID. It's worth noting the _id property used here. MongoDB's insertOne() API takes a single object and a callback as opposed to LevelDB's put() where we supplied the unique identifier key, user object, and callback. In MongoDB, this identifier key is, instead, placed within the object as the _id property, and we'll see how it retrieves a user with it in the getUser function. Apart from this change in ID logic, the rest of the function logic remains the same.

Let's now look at our getUser function. Previously, this was just one line, but to maintain the same API so that our handler code doesn't need to change, we have to add a little more logic here. Let's see what that looks like:

…
const getUser = function (userId, callback) {
  store.findOne({ '_id' : ObjectID(userId) }, (err, result) => {
    if (!result) {
      return callback(Boom.notFound());
    }
    result.id = result._id;
    delete result._id;
    return callback(null, result);
  });
};
…

As there're a few changes here, let's go through them in a little more detail.

We are now using Mongo's findOne() API as opposed to LevelDB's get(). As seen in the createUser function, this takes an object with the _id property instead of just a string as the identifier key.

The other difference is that LevelDB returns an error when an object was not found by its unique identifier. However, in MongoDB this is actually specified by the result variable of the findOne() callback being null. This means that we have to check for this, and return a Boom.notFound() error when result is null. Next we modify the stored object, so it is structured as our previous response was by removing the _id property from our object and adding the id property instead.

And that's it! Since we maintained all the same function signature and responses for createUser and getUser as before, we don't have to update our handler logic for either route. The full code for this example can be found in the examples for this chapter in the Getting Started with hapi.js repository on GitHub available at https://github.com/johnbrett/Getting-Started-with-hapi.js.

The 'M' in MVC

The previous section covered running basic queries against a database. While this is an easy way to persist and retrieve data from your chosen database, often, as the size of your application grows, so will the number of ways you interact with all this data. To manage this, often the best approach is to build an abstraction layer which handles all of these data queries. This is often referred to as the 'Model' in the very common Model View Controller (MVC)application design pattern.

If you are planning on building a Node application for the first time, I suggest you try following the MVC pattern initially, as it will aid greatly in trying to structure your application and keeping it manageable. It is probably the most well known design pattern in software development, and you'll find many tutorials on it with a quick Google search.

Often, when working with this 'Model' layer, libraries called ORMs are used. ORM stands for Object Relational Mapper. These ORM libraries usually offer a way to define a schema for what your data should look like, and then provide methods for storing, retrieving, and updating your data, which means that you can spend more time focusing on your business logic and functionality. There is a downside to ORMs in that they usually only account for the general use cases, and it can get very complex when trying to do something more complicated, or when trying to retrofit one to an existing application. If you want to investigate ORMs more, Mongoose is a very popular ORM for MongoDB (http://mongoosejs.com/). Hopefully, this has given you a good background in adding different databases to a hapi application.

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

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