DEV Community

loading...

Defining Routes in Hapi

Jalu Pujo Rumekso
Self-thaught web developer learning and writing Hapi/MySQL.
Originally published at jprumekso.github.io ・10 min read

Today Joe has plenty of time to learn more about Hapi. His target is learning how to add routes to his server so it can accept requests for creating and modifying data. And obviously, he also wants to be able to respond to the request back.

Before going straight to the documentation, Joe tries to guess it. He has a strong feeling that routing in Hapi has something to do with the server object. Because from the last learning session, he knew that the server object is the main application container. And if it has a bunch of properties such as server.info, then it should also have a bunch of methods and one of them must be a route-related method. That what's Joe thinking about.

Then he checks his hypothesis by visiting the documentation, especially the server object API section. And yes he is right. Indeed Hapi has server.route() which responsible for defining route endpoints. Finding his hypothesis get validated, he intends to use that educated guess more often in the future.

Creating Endpoints

The server.route() accepts an object as the only parameter with a format of {METHOD, PATH, HANDLER}, which is self-explanatory.

  • method is a string that refers to the name of the HTTP method Joe wants to respond. It accepts any valid HTTP method.
  • path is a string that refers to the actual path that the user will type in the browser (following the hostname).
  • handler is a function where he puts his code for that particular route

Given that new knowledge, Joe creates a test route that accepts GET request. Inside the handler, he returns a string that said 'OK'.

server.route({
  method: "GET",
  path: "/test",
  handler() {
    return "OK";
  },
});
Enter fullscreen mode Exit fullscreen mode

He starts his server and the terminal greets him with the server info which means his code from the previous coding session is still work. Then he opens his favorite web browser and types out the http://localhost:3000 followed by the route path /test at the address bar. When he hits enter, the browser shows 'OK'. Great!

Now Joe is ready to replicate the successful test to his initial mission. What is his mission anyway? The point of sale app! After reviewing his mission brief, Joe decides that he'll start with the store resource.

He wants the GET stores route to return an array of store objects. However, since he doesn't want to work with the database yet, he will just hardcoded it.

So here is Joe's simple pseudo database:

const stores = [
  {
    id: 1,
    name: "JoeTech Store 1",
    address: "East Java, Indonesia",
  },
  {
    id: 2,
    name: "JoeTech Store 2",
    address: "Lombok, Indonesia",
  },
  {
    id: 3,
    name: "JoeTech Store 3",
    address: "Bali, Indonesia",
  },
];
Enter fullscreen mode Exit fullscreen mode

And for the GET store route his code looks like this:

server.route({
  method: "GET",
  path: "/api/stores",
  handler() {
    return stores;
  },
});
Enter fullscreen mode Exit fullscreen mode

As we can see, he prefixes the stores path with api and uses a plural form for the resource name. Where does he get that information? His best friend, Google. And for the handler, he writes it in the shorthand syntax which is introduced in ES6.

One more question, where does he put that code? At first, he puts it inside the init function following the example at Hapi documentation. But when he found that his code still works even if it placed outside the init function, he chooses to place it there. He thinks that it's cleaner that way.

So now his app.js code looks like this:

const Hapi = require("@hapi/hapi");

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

const stores = [
  {
    id: 1,
    name: "JoeTech Store 1",
    address: "East Java, Indonesia",
  },
  {
    id: 2,
    name: "JoeTech Store 2",
    address: "Lombok, Indonesia",
  },
];

server.route({
  method: "GET",
  path: "/test",
  handler() {
    console.log("it works...");
    return "OK";
  },
});

server.route({
  method: "GET",
  path: "/api/stores",
  handler() {
    return stores;
  },
});

const init = async () => {
  try {
    await server.start();
    console.log("Server started...");
    console.log(server.info);
  } catch (error) {
    console.log(error);
  }
};

init();
Enter fullscreen mode Exit fullscreen mode

Then he opens his browser again and going to http://localhost:3000/api/stores. When he hits enter, the browser gives him this beautiful response, which is the exact same stores array he created before:

[
  {
    id: 1,
    name: "JoeTech Store 1",
    address: "East Java, Indonesia",
  },
  {
    id: 2,
    name: "JoeTech Store 2",
    address: "Lombok, Indonesia",
  }
]
Enter fullscreen mode Exit fullscreen mode

Testing API using REST Client Plugin

Joe realizes that testing his api using a browser will only work for GET endpoint. How about the other endpoints? From a lot of tutorials on Youtube, he knew that the most common way to test API is using Postman. But he wonders if there is a more simple approach to accomplish this task. Then he remembers something...

From his frontend development experience, he found that his favorite code editor, the VS Code, has many plugins available (thanks to the wonderful community). So he thought maybe there is a plugin for this particular task. So he goes to his best friend asking this matter. Here is what he asks Google: "how to make api call from vscode". Without much thinking, his friend gives him a lot of information. But there is one particular piece of info that Joe thinks would work for him, the REST Client plugin by Huachao Mao.

How to use REST Client plugin to test api?

The documentation says that first, he needs to create a file with .http extension. Joe names it ApiTest.http. Then he needs to write the request with a format like this METHOD URI HTTP/1.1. So here is what he writes for testing the test route:

GET http://localhost:3000/test HTTP/1.1
Enter fullscreen mode Exit fullscreen mode

And to send a payload we write the request like this:

POST http://localhost:3000/test HTTP/1.1
content-type: application/json

{
    "message": "Hello"
}
Enter fullscreen mode Exit fullscreen mode

Then to execute the request he needs to click the Send Request link at the top of the file.

Joe thinks that this plugin is surprisingly easy and intuitive. Thanks to this plugin, now Joe doesn't need to go back and forth between his code editor and Postman.

Query Parameter

Joe wants the user of his app to be able to search stores by name. He thinks this feature is important when his user has many stores. Also, it's pretty common by the way.

In this case, Joe needs to know how to get the value of the query parameter so when he types .../stores?name=something he can catch that 'something' and use it to filters the stores data.

The documentation says that Joe can access the query parameter's value from the request parameter object. It's available as the first parameter of the route handler function. It can be named anything, however, the common one is request or req for short.

Then Joe tries to implement it. Here is his code:

server.route({
  method: "GET",
  path: "/api/stores",
  handler(req) {
    const { name } = req.query;

    if (name) {
      return stores.filter((store) => store.name === name);
    }

    return stores;
  },
});
Enter fullscreen mode Exit fullscreen mode

Now he wants to test this new capability as well as testing the REST Client plugin for "a real use case". Here is what he writes at ApiTest.http:

GET http://localhost:3000/api/stores?name=JoeTech Store 1 HTTP/1.1
Enter fullscreen mode Exit fullscreen mode

And here is the result after he clicks the Send Request:

HTTP/1.1 200 OK
content-type: application/json; charset=utf-8
cache-control: no-cache
content-length: 68
accept-ranges: bytes
connection: close
Date: Wed, 17 Feb 2021 21:48:37 GMT

[
  {
    "id": 1,
    "name": "JoeTech Store 1",
    "address": "East Java, Indonesia"
  }
]

Enter fullscreen mode Exit fullscreen mode

This way, his GET stores route is capable of filtering the stores based on the name specified by the user via the query parameter. Awesome!

Defining Path Parameter

Listing all stores is only half useful if the user can't see each store's details. So now Joe wants to create a route for retrieving a single store by its id.

What does he need to know to achieve this goal? He needs to know how to define and access a path parameter in Hapi.

The documentation says that to define a path parameter Joe needs to wrap the name of the parameter with curly braces and simply include it in the path. In this case, what Joe wants to do is writing the route's path in this fashion: /api/stores/{id}. Then he can access that 'id' from the same request object above, specifically from the params property.

After understanding that explanation, Joe writes the GET single store route. His code looks like this:

server.route({
  method: "GET",
  path: "/api/stores/{id}",
  handler(req) {
    const { id } = req.params;
    return stores.find((store) => store.id === id);
  },
});
Enter fullscreen mode Exit fullscreen mode

His code looks good. He uses the req.params to access the id. Or more precisely, he uses ES6 destructuring to extract the id from req.params.

However, when he runs it, he gets this instead of the data of store with id 1:

HTTP/1.1 500 Internal Server Error
content-type: application/json; charset=utf-8
cache-control: no-cache
content-length: 46
Date: Wed, 17 Feb 2021 06:56:29 GMT
Connection: close

{
  "statusCode": 500,
  "error": "Internal Server Error",
  "message": "An internal server error occurred"
}
Enter fullscreen mode Exit fullscreen mode

And when he checks the terminal, it says:

Debug: internal, implementation, error
    Error: handler method did not return a value, a promise, or throw an error
    ...
Enter fullscreen mode Exit fullscreen mode

When he checks whether the id is obtained successfully using console.log(), it is. But why it still gives him an error? What's wrong?

Then he realizes that the path parameter is a string and he uses a strict equality operator to compare it with the store's id which is an integer, of course, he gets an error. So he fixes his code by parsing the path parameter's id to an integer. His code looks like this now.

server.route({
  method: "GET",
  path: "/api/stores/{id}",
  handler(req) {
    const { id } = req.params;
    return stores.find((store) => store.id === parseInt(id));
  },
});
Enter fullscreen mode Exit fullscreen mode

Now GET request to http://localhost:3000/api/stores/1 returns:

HTTP/1.1 200 OK
content-type: application/json; charset=utf-8
cache-control: no-cache
content-length: 46
Date: Wed, 17 Feb 2021 16:56:29 GMT
Connection: close

{
  "id": 1,
  "name": "JoeTech Store 1",
  "address": "East Java, Indonesia"
}
Enter fullscreen mode Exit fullscreen mode

It works!

Capturing Payload

Now Joe wants to work on the create store route. For this task, Joe needs to know how to capture the payload. So how does Hapi handling this matter?

Like the path parameter, the payload is also accessible via request object. For example, if Joe wants to echo out the user's payload at his POST /api/stores, then the code will look like this:

server.route({
  method: "POST",
  path: "/api/stores",
  handler(req) {
    return req.payload;
  },
});
Enter fullscreen mode Exit fullscreen mode

After understanding the method of getting the user's payload then Joe implements the "real" logic for the create new store route.

server.route({
  method: "POST",
  path: "/api/stores",
  handler(req) {
    const newStore = {
      id: stores.length + 1,
      name: req.payload.name,
      address: req.payload.address,
    };

    stores.push(newStore);

    return newStore;
  },
});
Enter fullscreen mode Exit fullscreen mode

Since he uses an array as dummy data, he only needs to catch the incoming store data and push it into the stores array. The data from the payload is passed unchanged except for the id which he manually adds by incrementing the array length by one. Of course, when he uses a database later the code will be more complex than this. After the store is added, he is returning the newly created store.

Now it's time to test it. Joe opens the ApiTest.http file and writes the following to create a post request to his new endpoint:

POST http://localhost:3000/api/stores HTTP/1.1
content-type: application/json

{
  "name": "Amalina",
  "address": "Jakarta, Indonesia"
}
Enter fullscreen mode Exit fullscreen mode

When he clicks Send Request, he gets the following response:

HTTP/1.1 200 OK
content-type: application/json; charset=utf-8
cache-control: no-cache
content-length: 46
Date: Thu, 18 Feb 2021 14:06:29 GMT
Connection: close

[
  {
    "id": 3,
    "name": "Amalina",
    "address": "Indonesia"
  }
]
Enter fullscreen mode Exit fullscreen mode

It means his code is working! Good job Joe!

There is only one route left, the edit store route which accepts a PUT request. Joe thinks that all of his routes, this is the most complex one. This route requires him to catch the path parameter as well as the payload. Thankfully, since he already nails the concept into his head, this complex task becomes easy for him. So here is his code:

server.route({
  method: "PUT",
  path: "/api/stores/{id}",
  handler(req) {
    const { id } = req.params;

    const theStore = stores.find((store) => store.id === parseInt(id));

    theStore.name = req.payload.name;
    theStore.address = req.payload.address;

    return theStore;
  },
});
Enter fullscreen mode Exit fullscreen mode

At the handler function, Joe grab the store's id from the path parameter. Then he uses that id to find the store. Then he updates the store data with the incoming payload.

He told by the internet that the PUT request should send a full resource. So even if he only wants to update certain property, he still need to send the full resource. So he needs to provide all the neccessary means to edit all store's properties: theStore.id, theStore.name and theStore.address. Surely he needs to find a more elegant way in the future when the store's details is not just name and address anymore.

Also he found many warnings to think that POST is exclusively for creating resource and PUT is exclusively for editing resource. Many people said that that's wrong! In fact, both POST and PUT can be used to create and edit a resource. Even though, in practice, he found many tutorials that simply map POST for create operation and PUT for edit operation.

Being a good beginner, who doesn't have much experiences yet, he tries to follow what the community said as well as what is make sense for him.

Then Joe tries his new route by sending a PUT request to modify the name of the store with id 1.

PUT http://localhost:3000/api/stores/1 HTTP/1.1
content-type: application/json

{
  "id": 1,
  "name": "J-Tech",
  "address": Indonesia
}
Enter fullscreen mode Exit fullscreen mode

And here is what he gets:

HTTP/1.1 200 OK
content-type: application/json; charset=utf-8
cache-control: no-cache
content-length: 46
Date: Fri, 19 Feb 2021 06:56:29 GMT
Connection: close

{
  "id": 1,
  "name": "J-Tech",
  "address": "Indonesia"
}
Enter fullscreen mode Exit fullscreen mode

Thank God, it works!

Having all store routes completed, Joe decides to call it a day. Joe feels happy as he has learned the skill to create routes in Hapi. For the next session, he wants to learn about validation in Hapi so he can make his app more stable.

Discussion (0)