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.
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.
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.
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.
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.
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.
A video version of this article can be seen below.