Creating a Node server with Hapi and Joi

Intro to creating a node server using Hapi instead of express. We will then look at validation use the Joi library..

Chris Ward

Hapi Hapi, Joi Joi

I love that the creators of these libraries must have been Ren & Stimpy fans. The names might be weird, strange, and hilarious, but there is nothing odd or bizarre going on here. Hapi is a node based web server that you can use instead of express. Joi is a validation library that you can use to validate path or query parameters. They integrate very nicely. You end up with very simple and easy to understand code. So lets have a look. You can checkout the github repository https://github.com/veggie2u/hapijoi, or follow the steps here. The repository has the code split into branches. The first step, or branch is called hello.

git checkout hello

To start from scratch, lets create a directory (hapijoi) and cd into that directory. Then…

npm init
npm install --save hapi

This will create your new node project and pull in the dependancies for the hapi server. The answers you gave during the npm init are not too important. This is what I have in the package.json file it created:

{
  "name": "hapijoi",
  "version": "1.0.0",
  "description": "Demo server using hapi and joi",
  "main": "index.js",
  "author": "Chris Ward",
  "license": "ISC",
  "repository": {
    "type": "git",
    "url": "https://github.com/veggie2u/hapijoi"
  },
  "dependencies": {
    "hapi": "15.1.1"
  }
}

Now for the actual server code. For this example, I am just going to use one index.js file. Enter each piece of code to create the most basic of servers. We will go through each part and what it does.

'use strict';
const Hapi = require('hapi');
const server = new Hapi.Server();
server.connection({ port: 9000 });

The first thing we do is require the hapi library. Without this, we have no access to the hapi library. We then create a server and start it on port 9000. Pretty simple. There are other things that can be set, but I will leave it to you look them up.

server.start(err => {
  if (err) {
    throw err;
  }
  console.log(`Server running at: ${server.info.uri}`);
});

Again, not much going on. We simply start the server and check if there was an error at startup. Once started we output to the console the server name and port so know how to access it on the browser. Now we can do something. The hapi server version of hello world.

// hello world
server.route({
  method: 'GET',
  path: '/',
  handler: function(request, reply) {
    reply('Hello, world!');
  },
});

This is our first route. Here we describe what url path the server should listen to, and what to do when it finds a match. In this simple case we are just listening to the default path of ’/‘. We get two inputs to our handler, request and reply. Request has several pieces of interesting data, like incoming query parameters. We will look at those in a minute. Reply? Shouldn’t it be response? The reply object is how you interact with the response. By calling the reply object with a string, it will be returned to the browser.

Have you run the server yet? Lets do that and see that it is working. From the console type:

node index.js

Did you get the console message that the server started and the port? You should be able to navigate to that URL in the browser now. Since we are only doing GET requests, the browser will be fine.

http://localhost:9000

Lets expand this a bit. Lets match a route on a different pattern and add a query parameter. If you are just following along with the project, you can see this next section on the branch routes. If you are entering each piece of code, just add it to index.js.

// hello query parameter
server.route({
  method: 'GET',
  path: '/hello',
  handler: function(request, reply) {
    let name = request.query.name
      ? encodeURIComponent(request.query.name)
      : 'world';
    reply(`Hello, ${name}!`);
  },
});

Our path is different. This route will match on /hello. To try this endpoint you will need to stop and restart the server. You can stop it with ctrl-c in the console. Also see that we are using a query param. They are found in the request object under request.query. Notice that we didn’t define any parameters. They will just show up here if entered in the URL.http://localhost:9000/hello?name=JimYou should get a “Hello Jim!” to come back. Leave off the name parameter and you will get “Hello world!“.

Now lets do the same thing with a URL parameter.

// hello path parameter
server.route({
  method: 'GET',
  path: '/hello/{name}',
  handler: function(request, reply) {
    reply('Hello, ' + encodeURIComponent(request.params.name) + '!');
  },
});

Again we are going with the /hello path, but we have added {name} to the end, so the url will look like this:http://localhost:9000/hello/JimThis will match a route that starts with /hello and adds a /something. We have called this something {name}, so it will show up in the request as request.params.name. Otherwise the code is the same.

So now we can create different routes, and use path and query parameters. How about a little more joi? One thing I really like about the hapi server is the built in joi validation library. Lets look at that. You can switch to the branch ‘validation’, or do the following at the command line to pull in the library.

npm install --save joi

This will add the joi dependency to the project in the package.json file. Next add the joi require to the top of the index.js file.

const Hapi = require('hapi');
const Joi = require('joi');

Then add the following route to the end of the same index.js file.

// hello query parameter with validation
server.route({
  method: 'GET',
  path: '/validate',
  config: {
    validate: {
      query: {
        name: Joi.string()
          .min(3)
          .max(10)
          .required(),
        age: Joi.number()
          .integer()
          .min(1)
          .max(50),
      },
    },
  },
  handler: function(request, reply) {
    let name = encodeURIComponent(request.query.name);
    let age = request.query.age ? encodeURIComponent(request.query.age) : null;
    let yourAge = age ? `Your age of ${age} is valid.` : '';
    reply(`Hello, ${name}! ${yourAge}`);
  },
});

I have added a new route, /validate so as not to collide with the previous endpoints. Other than that, the only difference from what we have seen is the new config block. There are a few things that can go in here, but to use joi, we add code to the validate block inside it. Because we are dealing with query parameters, we are one more level deep. config / validate / query / actualQueryParam.

In this code we have two query params we are validating. The first is the name, and we identify that it is a string. Joi supports method chaining, so we can say that the name should have at least 3 characters but no more than 10, and that name is required all with a few chained method calls. Similarly, we define age as a number, and an integer with a minimum value of 1, and a maximum value of 50. Note that age is not required. Lets see the results of hitting the new endpoint. Restart the server.

http://localhost:9000/validate

What do you get? A 400 error. A json parseable response tells you that the name is required.

{"statusCode":400,"error":"Bad Request","message":"child \\"name\\" fails because [\\"name\\" is required]","validation":{"source":"query","keys":["name"]}}

Now lets give a name and an age.

http://localhost:9000/validate?name=John?age=20
Hello, John! Your age of 20 is valid.

How about hitting the endpoint with an age of 60?

{"statusCode":400,"error":"Bad Request","message":"child \\"age\\" fails because [\\"age\\" must be less than or equal to 50]","validation":{"source":"query","keys":["age"]}}

Ok, this is getting long, but one more example. We can also validate path parameters too.

// hello path parameter with validation
server.route({
  method: 'GET',
  path: '/validate/{name}',
  config: {
    validate: {
      params: {
        name: Joi.string()
          .min(3)
          .max(10),
      },
    },
  },
  handler: function(request, reply) {
    reply('Hello, ' + encodeURIComponent(request.params.name) + '!');
  },
});

Validating path parameters works just the same, but instead of using a query block, with use a params block. Also note that I didn’t put the required() in there. Because this is a path parameter the only way we can access this code is if there is a name after /validate, so there is no need.

There are a ton of validations that can be done. You can check email, guids, or use a regex. Here is the API reference for Joi https://github.com/hapijs/joi/blob/v9.1.0/API.md

I was thinking… it would be nice to have actual logging instead of that console statement. Oh, and if we could show people our API… hmmmm These are all things that can be accomplished with hapi plugins. I will get to them next time.

Part 2 is up : Adding Logging and Swagger to a Hapi Node Server

Share this Post

Related Blog Posts

JavaScript

Adding Logging and Swagger to a Hapi Node Server

November 16th, 2016

Adding Logging and Swagger to a Hapi Node Server

Chris Ward
JavaScript

Getting started with VueJS

September 29th, 2016

VueJS is a hot contender with ReactJS, but includes most of that stuff you loved about Angular and less of the stuff you didnt.

Corey Webster
JavaScript

Using the Canvas API in Angular2

September 22nd, 2016

Describes a technique for using the Canvas API in an Angular2 Component.

Object Partners

About the author

Chris Ward

Sr. Consultant

Chris has been programming computers since the Commodore 64 as a kid. He has spent much of his career on the JVM with Java, Groovy, and Grails creating backend services for several companies in the the twin cities. Based on interest and a desire to learn new things, he has branched out into the Javascript realm. The last few years, Chris has been using Angular to create more functional and dynamic front end applications for his clients, as well as creating Node based services on the backend. Recently, just for fun, Chris has been getting into robotics and Iot.