Our website is made possible by displaying online advertisements to our visitors. Please consider supporting us by disabling your ad blocker.

Using Hapi.js, Mongoose, And MongoDB To Build A REST API

To continue on my trend of MongoDB with Node.js material, I thought it would be a good idea to use one of my favorite Node.js frameworks. Previously I had written about using Express.js with Mongoose, but this time I wanted to evaluate the same tasks using Hapi.js.

In this tutorial we’re going to develop a simple RESTful API using Hapi.js, Joi and Mongoose as the backend framework, and MongoDB as the NoSQL database. Rather than just using Hapi.js as a drop in framework replacement, I wanted to improve upon what we had previously seen, by simplifying functions and validating client provided data.

If you haven’t seen my previous tutorial, don’t worry because it is not a requirement. However, the previous tutorial is a valuable read if you’re evaluating Node.js frameworks. What is required is having a MongoDB instance available to you. If you’re unfamiliar with deploying MongoDB, you might want to check out my tutorial titled, Getting Started with MongoDB as a Docker Container Deployment or my tutorial titled, Developing a RESTful API with Node.js and MongoDB Atlas.

Creating a Hapi.js Project with MongoDB and Data Validation Support

With MongoDB available to us, we can create a fresh Hapi.js project with all the appropriate dependencies. Create a new project directory and execute the following commands:

npm init -y
npm install hapi joi mongoose --save

The above commands will create a new package.json file and install the Hapi.js framework, the Joi validation framework, and the Mongoose object document modeler (ODM).

We’re going to add all of our application code into a single project file. Create an app.js file and include the following boilerplate JavaScript code:

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

const server = new Hapi.Server({ "host": "localhost", "port": 3000 });

server.route({
    method: "POST",
    path: "/person",
    options: {
        validate: {}
    },
    handler: async (request, h) => {}
});

server.route({
    method: "GET",
    path: "/people",
    handler: async (request, h) => {}
});

server.route({
    method: "GET",
    path: "/person/{id}",
    handler: async (request, h) => {}
});

server.route({
    method: "PUT",
    path: "/person/{id}",
    options: {
        validate: {}
    },
    handler: async (request, h) => {}
});

server.route({
    method: "DELETE",
    path: "/person/{id}",
    handler: async (request, h) => {}
});

server.start();

We’ve added quite a bit of code to our app.js file, but it isn’t really anything beyond the Hapi.js getting started material found on the framework’s website. Essentially we’ve imported the dependencies that we downloaded, defined our servers settings, defined the routes which are also referred to as endpoints, and started the server.

You’ll notice that not all of our routes are the same. We’re developing a create, retrieve, update, and delete (CRUD) based REST API with validation on some of our endpoints. In particular we’ll be adding validation logic to the endpoints that save data to the database, not retrieve or remove.

With our boilerplate code in place, lets take a look at configuring MongoDB and adding our endpoint logic.

Interacting with the Database using the Mongoose ODM

Remember, I’m assuming you already have access to an instance of MongoDB. At the top of your app.js file, after you defined your server configuration, we need to connect to MongoDB. Inculde the following line to establish a connection:

Mongoose.connect("mongodb://localhost/thepolyglotdeveloper");

You’ll need to swap out my connection string information with your connection string information. When working with Mongoose, we need to have a model defined for each of our collections. Since this is a simple example, we’ll only have one model and it looks like the following:

const PersonModel = Mongoose.model("person", {
    firstname: String,
    lastname: String
});

Each of our documents will contain a firstname and a lastname, but neither of the two fields are required. These documents will be saved to the people collection which is the plural form of our ODM model.

At this point in time MongoDB is ready to be used.

It is now time to start developing our API endpoints, so starting with the creation endpoint, we might have something like this:

server.route({
    method: "POST",
    path: "/person",
    options: {
        validate: {}
    },
    handler: async (request, h) => {
        try {
            var person = new PersonModel(request.payload);
            var result = await person.save();
            return h.response(result);
        } catch (error) {
            return h.response(error).code(500);
        }
    }
});

We’ve skipped the validation logic for now, but inside our handler we are taking the payload data sent with the request from the client and creating a new instance of our model. Using our model we can save to the database and return the response back to the client. Mongoose will do basic validation against the payload data based on the schema, but we can do better. This is where Joi comes in with Hapi.js.

Let’s look at the validate object in our route:

validate: {
    payload: {
        firstname: Joi.string().required(),
        lastname: Joi.string().required()
    },
    failAction: (request, h, error) => {
        return error.isJoi ? h.response(error.details[0]).takeover() : h.response(error).takeover();
    }
}

In the validate object, we are choosing to validate our payload. We also have the option to validate the params as well as the query of a request, but neither are necessary here. While we could do some very complex validation, we’re just validating that both properties are present. Rather than returning a vague error to the user if either are missing, we’re returning the exact error using the failAction which is optional.

Now let’s take a look at retrieving the data that had been created. In a typical CRUD scenario, we can retrieve all data or a particular piece of data. We’re going to accommodate both scenarios.

server.route({
    method: "GET",
    path: "/people",
    handler: async (request, h) => {
        try {
            var person = await PersonModel.find().exec();
            return h.response(person);
        } catch (error) {
            return h.response(error).code(500);
        }
    }
});

The above route will execute the find function in Mongoose with no query parameters. This means that there is no criteria to search for which results in all data being returned from the collection. Similarly, we could return a particular piece of data.

If we wanted to return a particular piece of data we could either provide parameters in the find function, or use the following:

server.route({
    method: "GET",
    path: "/person/{id}",
    handler: async (request, h) => {
        try {
            var person = await PersonModel.findById(request.params.id).exec();
            return h.response(person);
        } catch (error) {
            return h.response(error).code(500);
        }
    }
});

In the above endpoint we are accepting an id route parameter and are using the findById function. The data returned from the interaction is returned to the client facing application.

With the create and retrieve endpoints out of the way, we can bring this tutorial to an end with the update and delete endpoints. Starting with the update endpoint, we might have something like this:

server.route({
    method: "PUT",
    path: "/person/{id}",
    options: {
        validate: {
            payload: {
                firstname: Joi.string().optional(),
                lastname: Joi.string().optional()
            },
            failAction: (request, h, error) => {
                return error.isJoi ? h.response(error.details[0]).takeover() : h.response(error).takeover();
            }
        }
    },
    handler: async (request, h) => {
        try {
            var result = await PersonModel.findByIdAndUpdate(request.params.id, request.payload, { new: true });
            return h.response(result);
        } catch (error) {
            return h.response(error).code(500);
        }
    }
});

Just like with the create endpoint we are validating our data. However, our validation is a little different than the previous endpoint. Instead of making our properties required, we are just saying they are optional. When we do this, any property that shows up that isn’t in our validator, we will be throwing an error. So for example, if I wanted to include a middle name, it would fail.

Inside the handler function, we can use a shortcut function called findByIdAndUpdate which will allow us to find a document to update and update it in the same operation rather than doing it in two steps. We are including the new setting so that the latest document information can be returned back to the client.

The delete endpoint will be a lot simpler:

server.route({
    method: "DELETE",
    path: "/person/{id}",
    handler: async (request, h) => {
        try {
            var result = await PersonModel.findByIdAndDelete(request.params.id);
            return h.response(result);
        } catch (error) {
            return h.response(error).code(500);
        }
    }
});

Using an id parameter passed from the client, we can execute the findByIdAndDelete function which will find a document by the id, then remove it in one swoop rather than using two steps.

You should be able to play around with the API as of now. You might want to use a tool like Postman before trying to use with a frontend framework like Angular or Vue.js.

Conclusion

You just saw how to create a REST API with Hapi.js and MongoDB. While we used Mongoose and Joi to help us with the job, there are other alternatives that can be used as well.

While Hapi.js is awesome, in my opinion, if you’d like to check out how to accomplish the same using a popular framework like Express.js, you might want to check out my tutorial titled, Building a REST API with MongoDB, Mongoose, and Node.js. I’ve also written a version of this tutorial using Couchbase as the NoSQL database. That version of the tutorial can be found here.

As a side note, I’ve also released an eBook and video course titled, Web Services for the JavaScript Developer which goes into REST and GraphQL API development with finer detail.

A video version of this tutorial can be seen below.

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.