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 Joidocumentation 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.
No comments:
Post a Comment