Building A RESTful API With Node.js And Hapi Framework

Back when I was really getting into the swing of Node.js, I had written about creating a simple RESTful API that made use of the Express framework. Express was, and still is, one of the most popular frameworks for creating web applications with Node.js. However, this doesn’t mean it is the best solution.

Recently I’ve been hearing a lot around Hapi for Node.js. The common feedback that I hear is that it is specifically designed for creating RESTful web services making them significantly easier to create without as much boilerplate code.

We’re going to see how to create a simple API using Hapi as well as packages such as Joi for request validation.

Before we start writing any code, you should note that we won’t be using a database in this example. All data will be mocks so that we stay focused on learning Hapi rather than learning a database. If you want to see how to use Hapi with a NoSQL database, check out this previous tutorial that I had written.

Create a Fresh Node.js Project with Dependencies

To keep this tutorial easy to understand, we’re going to start by creating a fresh project and work our way into the development of various API endpoints. Assuming that you have Node.js installed, execute the following commands:

npm init -y
npm install hapi joi --save

The above commands will create a new project package.json file and install Hapi and Joi. Remember that Hapi is our framework and Joi is for validation.

For this project, all code will be in a single file. Create an app.js file within your project.

Adding the Boilerplate Server Logic

The first step in building our project is to add all the boilerplate logic. This includes importing our downloaded dependencies and defining how the application will be served when run.

Open your project’s app.js file and include the following JavaScript code:

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

const server = new Hapi.Server();

server.connection({ "host": "localhost", "port": 3000 });

server.start(error => {
    if(error) {
        throw error;
    }
    console.log("Listening at " + server.info.uri);
});

In the above code we are defining our server listening information, which is localhost and port 3000, as well as starting the server.

If you’re familiar with Express, you’ll notice that we didn’t have to include the body-parser package which is common for being able to receive request bodies. Hapi will take care of this for us.

At this point we have a server, but no endpoints to hit.

Designing the RESTful API Endpoints with Hapi

Most web services will have full create, retrieve, update, and delete (CRUD) capabilities. Each type of access or data manipulation should have its own endpoint for access. However, before we get deep into it, let’s create a basic endpoint for the root of our application.

Within the app.js file, include the following:

server.route({
    method: "GET",
    path: "/",
    handler: (request, response) => {
        response("Hello World");
    }
});

If someone tries to access http://localhost:3000/ from their client, the plain text Hello World will be returned.

Now let’s say we are working with user accounts. In this scenario we’ll probably need a way to access a particular account in our database. To make this possible, we might have something that looks like the following:

server.route({
    method: "GET",
    path: "/account/{username}",
    handler: (request, response) => {
        var accountMock = {};
        if(request.params.username == "nraboy") {
            accountMock = {
                "username": "nraboy",
                "password": "1234",
                "twitter": "@nraboy",
                "website": "https://www.thepolyglotdeveloper.com"
            }
        }
        response(accountMock);
    }
});

In the above route we are making use of a URL parameter. While we’re not yet validating the request data, we are returning data based on if the request data has a match to our mock data.

We’ll get to the validation stuff soon.

In combination with retrieving data, we’re going to want to be able to create data via an HTTP request. This is typically done through a POST request. Take a look at the following example:

server.route({
    method: "POST",
    path: "/account",
    handler: (request, response) => {
        response(request.payload);
    }
});

In the above example we are expecting a POST request with an HTTP body. Hapi interprets the body as a request payload. In this example we are only returning the payload that was sent to us.

This is where things get a little interesting, and in my opinion, super convenient.

Validating URL Parameters, Query Parameters, and Request Bodies with Joi

Validation is a pain in the butt. If you saw, Create A Simple RESTful API With Node.js, which uses Express, you’ll remember I had a lot of conditional statements. This approach is not very pleasant to write or maintain.

Instead we can use Joi for Hapi.

Let’s revisit the POST endpoint for creating a user account. This time around we want to make sure that certain properties exist in our payload and that they meet a certain set of criteria.

server.route({
    method: "POST",
    path: "/account",
    config: {
        validate: {
            payload: {
                firstname: Joi.string().required(),
                lastname: Joi.string().required(),
                timestamp: Joi.any().forbidden().default((new Date).getTime())
            }
        }
    },
    handler: (request, response) => {
        response(request.payload);
    }
});

In the above code, we want to make sure that a firstname and lastname exist in our request.payload and that they are strings. If we forget to include one of those properties, we’ll get a response that looks like the following:

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

The firstname and lastname properties are not the only requirements for the request. If the user decides to provide a timestamp, an error will be thrown. We want to forbid people from providing this information. As long as they do not provide this timestamp, we will default the value on our own to the current time.

The request payload isn’t the only thing that we can validate. We can also validate query parameters and URL parameters. Take the following for example:

server.route({
    method: "PUT",
    path: "/account/{username}",
    config: {
        validate: {
            payload: {
                firstname: Joi.string().required(),
                lastname: Joi.string().required(),
                timestamp: Joi.any().forbidden().default((new Date).getTime())
            },
            params: {
                username: Joi.string().required()
            },
            query: {
                alert: Joi.boolean().default(false)
            }
        }
    },
    handler: (request, response) => {
        response(request.payload);
    }
});

The above example, while very random, has validators on the payload, params, and query items in the request.

The validation that I chose to use is incredibly simplistic compared to the options that you have. The Joi documentation is quite thorough and there are a lot of different validation techniques you can apply. It makes things significantly easier than using a bunch of conditional statements.

Conclusion

You just saw how to create a simple RESTful API using Node.js, Hapi, and Joi, as well as some mock data. The combination of Hapi and Joi makes API creation very fast with minimal amounts of code compared to the alternatives. If you’re interested in using a NoSQL database with your API, check out a previous example that I wrote about.

If you’d like to test the API endpoints created in this example, check out Postman which I explain in a previous tutorial on the subject titled, Using Postman To Troubleshoot RESTful API Requests.

Nic Raboy

Nic Raboy

Nic Raboy is an advocate of modern web and mobile development technologies. He has experience in Java, JavaScript, Golang and a variety of frameworks such as Angular, NativeScript, and Apache Cordova. Nic writes about his development experiences related to making web and mobile development easier to understand.

Search

Follow Us

Subscribe

Subscribe to my newsletter for monthly tips and tricks on subjects such as mobile, web, and game development.

Subscribe on YouTube

Support This Site