samrocksc
8/25/2015 - 12:02 AM

A practical introduction to building a RESTful API with the hapi.js server framework for Node.js

A practical introduction to building a RESTful API with the hapi.js server framework for Node.js

This tutorial uses the "Sample hapi.js REST API" project.

Take a look at: https://github.com/agendor/sample-hapi-rest-api/

##Topics

  • Introduction
  • Installing Node.js
  • Installing MySQL
  • Setting-up the project
  • Folder and file structure
  • The API
    • The Server
    • Authentication
    • Routing
    • The Controller
      • Server Plugins
    • The Model
    • Validation
    • Data manipulation

##Introduction

hapi.js is an open source rich framework built on top of Node.js for building web applications and services. Was created by Eran Hammer - currently Sr. Architect of Mobile Platform at Walmart - and powers up all Walmart mobile APIs. It was battle-tested during Walmart Black Friday without any problems. They also plan to put hapi.js in front of every Walmart ecommerce transaction.

In this tutorial, we'll use an open source project called sample-hapi-rest-api.

##Installing Node.js

The first thing we need to do is install Node.js. Node.js is a platform built on Chrome's JavaScript runtime for easily building fast, scalable network applications. Node.js uses an event-driven, non-blocking I/O model that makes it lightweight and efficient, perfect for data-intensive real-time applications that run across distributed devices. To install Node.js, open a terminal tab and run the following commands:

sudo apt-get update
sudo apt-get install nodejs

##Installing MySQL

In this project, we consume a MySQL database, if you don't have MySQL installed in your computer, you may install it by running the commands:

sudo apt-get update
sudo apt-get install mysql-server
sudo apt-get install mysql-client

During the installation process you will be prompted to enter a password for the MySQL root user.

##Setting-up the project

Ok, now we've installed Node.js and MySQL, we're going to setup the project. With this project, you can build a production-ready RESTful API that consumes a MySQL database built with hapi.js framework.

Follow the instructions presented in the project page: https://github.com/agendor/sample-hapi-rest-api

After setting-up the project, continue to the next sections.

##Folder and file structure

To begin with, the important parts of the structure for now are:

package.json
index.js
node_modules/
src/
|-config/
  |-constants.js
|-controllers/
|-dao/
|-middleware/
  |-basic-auth.js
  |-db.js
|-models/
|-routes/
|-util/
|-validate/
  • package.json: Holds project configuration.
  • index.js: The starting point for the API.
  • node_modules/: All modules described in package.json will be automatically placed here using npmcommands such as npm install mysql --save.
  • src/: Our source code base folder.
  • src/config/: Application level configuration files.
  • src/config/constants.js: Application level configuration constants.
  • src/controllers/: Controllers modules/files.
  • src/dao/: Data Access Object modules/files.
  • src/middleware/: Holds modules/files that deals with different code environments.
  • src/middleware/basic-auth.js: Our Basic Authentication strategy module. We'll see it latter.
  • src/middleware/db.js: Abstracts our database initialization and manipulation.
  • src/models/: Modules/files abstraction of our database schema.
  • src/routes/: Modules/files that know which controllers should handle the incoming requests.
  • src/util/: Help us with mixins methods.
  • src/validate/: Knows how the incoming request should behave.

##The API

In every Node.js project, we have to have a starting point. In this project, the starting point is the index.js file, which the main parts are:

//1.
var Hapi = require('hapi');

//2.
var server = Hapi.createServer(host, port, options);

//3.
for (var route in routes) {
    server.route(routes[route]);
}

//4.
server.start();
  1. Assign hapi.js module to a Hapi variable. The module is present in the node_modules folder, so we don't need to specify a path for that.
  2. Creates a server instance.
  3. Dynamically adds all the routes (end-points) to the server instance. The routes are stored in the src/routes folder.
  4. Starts the hapi.js server.

##Authentication

It's very common in an API to validate the client's request against some credentials. In this tutorial, the credentials will be a user email and password stored in our MySQL database table user. For this part, we are using hapi-auth-basic module, so when the client makes a request, this module will handle the authentication strategy. Continuing in the index.js file, we've added the hapi-auth-basic to a server pack. A server pack groups multiple servers into a single pack and enables treating them as a single entity which can start and stop in sync, as well as enable sharing routes and other facilities (like authentication). The following code in index.js is responsible for this assignment:

var basicAuth = require('src/middleware/basic-auth');
...
server.pack.require('hapi-auth-basic', function (err) {
    server.auth.strategy('simple', 'basic', true, {
        validateFunc: basicAuth
    });
});

Look that the basicAuth handler is a module/function stored in src/middleware/basic-auth that we've created to validate the request credentials sent by the client.

This goes to the database and lookup for a user with the email and password informed.

You can test that by accessing: http://localhost:8000/tasks (if you haven't changed the port configuration), and typing:

##Routing

The routing with hapi.js is pretty practical. Take a look at the file src/routes/task.js. You'll see that we've configured all the routes (end-points) our task module needs with a list of JSON objects. Taking the first route as an example:

method: 'GET',
path: '/tasks/{task_id}',
config : {
    handler: taskController.findByID,
    validate: taskValidate.findByID
}
  • method: the HTTP method. Typically one of 'GET', 'POST', 'PUT', 'PATCH', 'DELETE', 'OPTIONS'. Any HTTP method is allowed, except for 'HEAD'.
  • path: the absolute path used to match incoming requests (must begin with '/'). Incoming requests are compared to the configured paths based on the server router configuration option. The path can include named parameters enclosed in {} which will be matched against literal values in the request as described in Path parameters.
  • config: additional route configuration (the config options allows splitting the route information from its implementation).
  • config.handler: an alternative location for the route handler function. Same as the handler option in the parent level. Can only include one handler per route.
  • config.validate: is used to validate the incoming requests. We'll go deep later.

##The Controller

The controller modules are used to mediate the communication between requests and data manipulation. Open the src/controllers/task.js file. See that we've implemented a method to handle each type of request routed by the src/routes/task.js route. Every controller module will use a helper module called ReplyHelper (src/controllers/reply-helper.js). Here is nothing much about hapi.js.

###Server Plugins

Reopen the file src/controllers/task.js and look at the part:

findByID: function findByID(request, reply) {

    var helper = new ReplyHelper(request, reply);
    var params = request.plugins.createControllerParams(request.params);

    taskDAO.findByID(params, function (err, data) {
        helper.replyFindOne(err, data);
    });
}

See that we are calling the createControllerParams method from the namespace request.plugins?

Plugins provide an extensibility platform for both general purpose utilities such as batch requests and for application business logic.

This method was declare in index.js as:

server.ext('onRequest', function(request, next){
    request.plugins.createControllerParams = function(requestParams){
        var params = _.clone(requestParams);
        params.userId = request.auth.credentials.userId;
        return params;
    };
    next();
});

So it's basically a better way to say that every request object should be able to add the ID of the authenticated user to some requestParams object and return it to the caller. We'll use this ID to make some queries to MySQL.

##The Model

As we are building an API to consume a MySQL Database, we need a place to store the same schema as defined in the database tables. The place for that are the Model modules. Open up src/models/task.js and take a look, we'll see how to make sure our Task objects have the right values in its properties.

##Validation

If you haven't read the previous section, open the src/models/task.js file:

"use strict";

var _ = require('underscore');
var Joi = require('joi');

function TaskModel(){
    this.schema = {
        taskId: Joi.number().integer(),
        description: Joi.string().max(255)
    };
};

module.exports = TaskModel;

We are using Joi module. As described in its repository, Joi is:

Object schema description language and validator for JavaScript objects.

Every request we receive in our end-points is validated using Joi. So every resource have a ResourceValidate module (look at src/validate/task.js which describes a validation schema for every route/method/end-point.

##Data manipulation

There is a very good module created by Felix Geisendörfer to manipulate MySQL databases. It's called node-mysql and we use it in our project.

If you take a look at src/dao/task.js, you'll see that we have a corresponding method for every end-point a client can request. There is nothing about hapi.js in here :)

##Conclusion

As we started to build the API for http://www.agendor.com.br, we couldn't find an example of files and folders structure, using the hapi.js framework and consuming a MySQL DB. So we've built our own, looking at lots of different Node.js projects.

We are a case of migrating from PHP to Node.js to place an API in front of our already existent MySQL database. We hope this project can serve as a starting point for many other teams that find themselves at this same cenario.

##References