Hapi.js — Project Structure and Best Practices (Part 2)

Arpit Khandelwal
The Resonant Web
Published in
11 min readAug 1, 2019

This is the second part of the series on hapi.js, the first part explained what hapi is all about and basics of its plugins and request lifecycle along with a piece of code demonstrating how to create a “hello world” hapi server. This part explains about a starter-kit prepared by me using hapi v18.x which can help save a substantial amount of development effort towards doing the foundational work for a scalable web application.

The Starter-Kit

Most modern web applications need two front-ends for users — one that’s browser based (for desktops/laptops) and another which is usually a (native or hybrid) mobile application. Browser-based frontends are typically used for administrative purposes (admin portals) and have limited features compared to their mobile counterparts, which are generally designed for end-users.

The concept behind this starter-kit was to provide developers with a way to satisfy both of these needs by having the same hapi server cater to both kinds of consumers, while reducing the redundancies of controllers and routes at the same time.

GitHub Repo

The codebase for this writeup can be cloned from the following GitHub repo:

Starter-Kit Details

This starter-kit serves as a great starting point for all the hapi developers who were looking for a hapi based platform as their production server. Once successfully deployed, this kit gives developers access to the following two modes of interaction with the server:

  1. http://localhost:<port> — Serves web requests
  2. http://localhost:<port>/api — Serves rest api requests

Web and REST APIs both have their independent authentication mechanisms. This kit assumes the availability of ‘MongoDB’ installation on the localhost where the server will run or the use of docker-compose to boot up a mongodb container and link the application with it within the realm of docker.

The starter-kit contains basic features such as user management with login, logout, password reset, profile view, dashboard view etc. Forgot password implementation is done using ‘nodemailer’ (sends an email with reset password link with a unique token that expires in an hour). All the environment variables are managed using ‘dotenv’ node package for development, the same will need to be configured at the host server in the production environment.

Libraries Used

The starter-kit uses the following primary libraries for satisfying various requirements:

  • Hapi — Server side framework
  • Docker — Docker container support
  • Handlebar — HTML templating engine
  • Mongoose — Mongo database ORM
  • SASS — CSS preprocessor
  • WebPack — Asset pipeline
  • Dotenv — Environment variable emulator
  • Good — Logger mechanism
  • JWT — Authentication mechanism for APIs
  • Config — Configuration Handler
  • Nodemailer — Module to send emails
  • Mocha — Unit test framework

Opinionated Application Structure

The kit is designed in such a way that all the relevant artefacts are organised in their separate folders. You will see app, assets and config being the topmost container directories along with the root directory. A detailed view of the application structure is specified below:

├── app
│ ├── controllers
│ │ ├── api // Controllers
│ │ └── web // Controllers
│ ├── helpers // Helpers
│ ├── models // All mongoose models are defined here
│ ├── routes // All app routes are defined here
│ │ ├── mobileApi // RESTAPI routes for multiple versions
│ │ │ ├── v1 // Routes for version1
│ │ │ └── v2 // Routes for version2
│ │ └── webApi // WEB api routes.
│ └── templates // Server-rendered handlebar templates
├── assets // Contains all static resources
│ ├── fonts // Fonts used in application
│ ├── images // Images used in application
│ ├── scripts // Scripts used in application
│ │ ├── js // User defined scripts
│ │ └── vendor // Vendor scripts
│ └── styles // All SASS stylesheets
├── config // Contains all app configurations
│ ├── assets.js // Assets configuration file
│ ├── config.js // Application configuration
│ ├── default.json // Configuration file
│ ├── manifest.js // App manifest file
│ ├── meta.js // App metadata file
│ └── ssl // Contains ssl certificates
├── lib // Core application lib/plugins
├── logs // Contains app log file
├── .gitignore // standard git ignore file
├── .babelrc // Babel config
├── .eslintrc // Define eslint rules
├── .eslintignore // Ignores certain files for eslint rules
├── Dockerfile // Standard docker file
├── docker-compose.yml // Standard docker compose file
├── server.js // Contains all app configurations
├── .env // dotenv configuration
└── test
├── testcases // Test cases organised by module names
└── test.js // Test file

The best thing about using opinionated structures is that there is basically one way (the right way) to do things and trying to do it differently will be difficult (and frustrating). Doing things the right way can make it very easy to develop with the software as the number of decisions that you have to make is reduced and the ability of the software designers to concentrate on making the software work is increased. This kit follows the same principle, I have used this kit in a variety of projects with a great amount of success due to the homogeneity of the code foundation and only variable being the business logic.

If you consider development in a big team environment then opinionated frameworks like these actually help you a lot as a technical lead. One thing you are always sure is about the quality of the code being written and the way different pieces join hands to combine into a big application. Without such frameworks, code review can be a nightmare considering there are global teams with developers having different thought process and ability to adapt certain coding styles.

Application Flow

Let’s walk ourselves through the signup flow in the codebase to understand how the different directories, plugins and scripts come into picture for a POST api request for signup from a mobile device.

The first point of contact in the application is the routes directory. It contains the routing information for the various HTTP requests. All the routers are configured as independent plugins, here is how the source code of the routes/mobileApi/v1/signup.js plugin looks like:

signup.js (router)

Whenever a POST request for signup is sent to the server the, ‘signup_routes_v1’ plugins catches it and contacts the /controllers/api/signup controller for handling the request.

signup.js (controller)

Validating data can be very helpful in making sure that our application is stable and secure. hapi allows this functionality by using the module Joi, which allows us to create our validations with a simple and clear object syntax. In this case the validate routine inside the userSignUp function does the same by applying rules on the email field, password and confirm password fields and the name field. If for any reasons the applied validations fail, a HTTP 400 response is immediately returned from the server.

Once the validations pass, the async handler function comes into picture. It is dependent on a helper /helpers/signup.js which is defined in the helpers directory at the root.

signup.js (helper)

The helper takes help from the User.js mongoose model to search if the passed user details match an existing user of the application, if it does then a HTTP 409 response is sent back to the caller which signifies that this is an existing user and system cannot allow its creation. However, if the details don’t match any existing user in the mongodb, then the save method results in creating one and returns a HTTP 201 stating the signup was successful.

This entire flow could be tested using the swagger documentation as follows:

POST request : SignUp
SignUp Response

The other API flows are very similar to the signup flow, hopefully the illustrations above have given you fair bit of insight on understanding the code base better.

Authentication

Both of the servers spun up in this kit have independent authentication measures in place. The web application server uses cookie-based authentication, while the API server uses JWT authentication for rest API access. The deployment and application management is done via standard node scripts, webpack and forever.

Mobile api authentication request handler:

Web authentication request handler:

Glued Server

A question you might be having right now is where and how the hapi server has been defined. The answer is, its not defined in the traditional manner which most tutorials suggest you to do:

const Hapi = require(‘hapi’); 
const server = Hapi.server({
port: 3000,
host: ‘localhost’
});

Rather, the server is defined inside the server.js file as follows:

Glue is a plugin that works with hapi to configure a server relatively easy, it works by exporting a single function named compose to your hapi server. Inside of compose are two parameters that work to configure your server automatically, from server connections to server plugins.

The manifest file contains declarations of plugins created throughout the codebase along with the configurations passed to them declaratively inside JSON objects. We will discuss more about the configurations in the next section, for now all you need to know is we are using Glue to create the hapi server, glue needs a pre defined (and of course opinionated) config file (called as manifest.js in this case) and then you can simply invoke the server.start() method to boot the hapi server.

Configurations

There are two important configuration files present in the starter-kit:

  1. config/config.js
  2. config/manifest.js

config.js

The config.js file contains the configurations needed by various plugins defined in the codebase. For ex. the mongoose json object contains the mongodb connection uri according to the environment (default, test, prod), the email object contains the various settings needed to send an email from our starter-kit etc

The manifest.js file on the other hand contains the plugin declarations and provides them with the configuration needed defined in the config.js.

manifest.js

For registering any new route using the structure explained so far, the last thing you will need to do is register the plugin in this manifest so that Glue can compose the hapi server with the set of plugins at runtime. If you have defined the plugin properly but forgot to include it here, Glue has no way of knowing that your new plugin needs to be registered with hapi and hence it won’t show up.

Unit Testing

If you are using the Chrome browser for unit testing the application, you can use the ‘node-inspector’ plugin pre-configured with this app for debugging purposes. This kit supports writing unit test cases using ‘Mocha’ and ‘Supertest’ node packages. To execute the test cases please run the following command:

# Test the server
$ npm test

Logging

The logging aspect for this kit is covered with ‘Good’ which is a hapi plugin for logging. The configurations are organised using ‘Glue’ along with ‘Confidence’ (as explained earlier) which makes it very easy to switch between different plugins for the various functionalities while keeping the hapi’s ‘configuration over convention’ theme intact.

We have used the following configuration for the logging aspects of the starter-kit using Good (defined in config.js):

“myConsoleReporter” configuration above instructs hapi to log the request details on the console (stdout), whereas the “myFileReporter” tells hapi to log the same set of details in the /logs/log file.

Swagger Documentation

The APIs are well documented for unit testing and sharing with UI team using Swagger. Please use the following URL for accessing the same:

http://localhost:8000/documentation#/

Standard hapi Packages Used

The following packages/plugins were used from the standard hapi.js ecosystem in the development of this kit:

  1. Crumb
  2. Glue
  3. Good
  4. Good-Console
  5. Hoek
  6. Hapi-auth-cookie
  7. Inert
  8. Yar
  9. Wreck

Webpack v4

As you might already know, Webpack is an open-source JavaScript module bundler. Its main purpose is to bundle JavaScript files for usage in a browser, yet it is also capable of transforming, bundling, or packaging just about any resource or asset.

In case of this starter-kit too, we have used webpack to create following two important files:

  1. bundle.js
  2. vendor.js

Along with these files, webpack also bundles the entire css scripts present in the development folders into a single file “index.css” and dumps all of them in the .build directory which gets created once webpack runs as a prestart script for the server execution. We have used v4 for this kit and the webpack configuration looks as follow:

webpack.config.js

Running the Server Locally

To run the server in standalone mode use either of the following commands:

# Install dependencies
$ npm install

# Run the node server in dev mode
$ npm start

# Run the node server in production mode
$ npm run prod

Running the Server in Docker Container

Please ensure that docker and docker-compose are installed on your system.

The contents of the Dockerfile are as follows:

The docker-compose file is organised as follows:

Steps to run app in docker container:

# cd to project dir# Create build
$ docker-compose build
# Start the server in daemon thread
$ docker-compose up -d
# Stop the server
$ docker-compose down

In case you are willing to use this project as is (i.e. without external mongodb installation), the docker-compose provided with this project should suffice. It brings along a mongodb service which stores the data in the /db/data directory. But in case you wish to use your existing MongoDB installation then please remove the mongodb service from the docker-compose.yml file and correct the database_url environment variable.

Application Screenshots

Here are some of the screenshots of the default features being provided along with the starter-kit:

Login Page

Sign-up Page

Forgot Password Page

Dashboard View

Settings View

Conclusion

As you can see, the starter-kit is designed to save you hours of coding time by giving you a streamlined, efficient and clean-coded interface and architecture to base your own development on. Try it out and be sure to let me know your feedback.

Hapi coding everyone!

If it was interesting or helpful to you, please do press the 👏 clap button and help others find this story too.

Did you know you could give up to 50 claps?

--

--