Cloud Native programming with Golang
上QQ阅读APP看书,第一时间看更新

Implementing a Restful API

The first step to utilize the package is to make use of the go get command in order to obtain the package to our development environment:

$ go get github.com/gorilla/mux

With that, the mux package will be ready to use. In our code, we can now import the mux package to our web server code:

import "github.com/gorilla/mux"

Inside our code, we now need to create a router using the Gorilla mux package. This is accomplished via the following code:

r := mux.NewRouter()

With this, we will get a router object called r, to help us define our routes and link them with actions to execute.

From this point forward, the code will differ based on the microservice in question since different services will support different routes and actions. Earlier in this chapter, we covered the following four different types of services to use in our MyEvents application—Web UI service, search microservice, bookings microservice, and events microservice. Let's focus on the events microservice.

The events microservice will need to support a RESTFul API interface that is capable of doing the following:

  • Searching for events via an ID or event name
  • Retrieving all events at once
  • Creating a new event

Let's focus on each one of those tasks. Since we are in the process of designing a web RESTful API for the microservice, each task will need to translate into an HTTP method combined with a URL and an HTTP body if need be.

The following is the break down:

  •  Searching for events via:
    • ID: Relative URL is /events/id/3434, method is GET, and no data is expected in the HTTP body
    • Name: Relative URL is /events/name/jazz_concert, method is GET, and no data is expected in the HTTP body
  •  Retrieving all events at once: Relative URL is /events, method is GET, and no data is expected in the HTTP body
  •  Creating a new event: Relative URL is /events, the method is POST, and expected data in the HTTP body needs to be the JSON representation of the new event we would like to add. Let's say we would like to add the event of opera aida that would play in the U.S., then the HTTP body would look like this:

Now, if you look at the HTTP translations of each task, you will notice that their relative URLs all share a common property, which is the fact that it starts with /events. In the Gorilla web toolkit, we can create a subrouter for the /events—relative URL. A subrouter is basically an object that will be in charge of any incoming HTTP request directed towards a relative URL that starts with /events.

To create a subrouter for URLs prefixed with /events, the following code is needed:

eventsrouter := r.PathPrefix("/events").Subrouter()

The preceding code makes use of the router object we created earlier, then calls the PathPrefix method, which is used to capture any URL path that starts with /events. Then, finally, we call the Subrouter() method, which will create a new router object for us to use from now on to handle any incoming requests to URLs that start with /events. The new router is called eventsrouter.

Next, the eventsrouter object can be used to define what to do with the rest of the URLs that share the /events prefix. So, let's revisit the list of HTTP translations for our tasks and explore the code needed to get them done:

  1. Task: Searching for events via:
    • id: Relative URL is /events/id/3434, the method is GET, and no data is expected in the HTTP body
    • name: Relative URL is /events/name/jazz_concert, the method is GET, and no data is expected in the HTTP body:
eventsrouter.Methods("GET").Path("/{SearchCriteria}/{search}").HandlerFunc(handler.findEventHandler)

The handler object in the preceding code is basically the object that implements the methods that represent the functionality that we expect to be mapped to the incoming HTTP request. More on that later.

  1. Task: Retrieving all events at once—Relative URL is /events, the method is GET, and no data is expected in the HTTP body:
eventsrouter.Methods("GET").Path("").HandlerFunc(handler.allEventHandler)
  1. Task: Creating a new event—Relative URL is /events, the method is POST, and expected data in the HTTP body needs to be the JSON representation of the new event we would like to add:
eventsrouter.Methods("POST").Path("").HandlerFunc(handler.newEventHandler)

For tasks 2 and 3, the code is self-explanatory. The Gorilla mux package allows us access to Go methods that eloquently define the properties of the incoming HTTP request that we would like to capture. The package also allows us to chain the calls together in a line to efficiently structure our code. The Methods() call defined the expected HTTP method, the Path() call defined the expected relative URL path (note that we placed the call on the eventsrouter object, which would append /events to the relative path defined in the Path() call), and finally comes the HandlerFunc() method.

The HandlerFunc() method is how we will link the captured incoming HTTP request with an action. HandlerFunc() takes an argument of the func(http.ResponseWriter, *http.Request) type. This argument is basically a function with two important arguments—an HTTP response object that we need to fill with our response to the incoming request and an HTTP request object, which will contain all the information about the incoming HTTP request.

The functions we pass to HandlerFunc() in the preceding code is handler.findEventHandler, handler.allEventHandler, and handler.newEventHandler—all support the func(http.ResponseWriter, *http.Request) signature. handler is a Go struct object, created to host all those functions. The handler object belongs to a custom Go struct type called eventServiceHandler.

In order for the eventServiceHandler type to support the HTTP handlers for tasks 1, 2, and 3, it needs to be defined like this:

type eventServiceHandler struct {}

func (eh *eventServiceHandler) findEventHandler(w http.ResponseWriter, r *http.Request) {

}

func (eh *eventServiceHandler) allEventHandler(w http.ResponseWriter, r *http.Request) {

}

func (eh *eventServiceHandler) newEventHandler(w http.ResponseWriter, r *http.Request) {

}

In the preceding code, we created eventServiceHandler as a struct type with no fields, then, we attached three empty methods to it. Each one of the handler methods supports the function signature needed to become an argument for the Gorilla mux package HandlerFunc() method. The detailed implementation of each one of the eventServiceHandler methods will be discussed in more detail in this chapter when we cover the persistence layer of our microservice.

Now, let's go back to task 1. The /{SearchCriteria}/{search} path in our code represents the equivalent of the /id/2323 path to search for the event ID 2323, or the path /name/opera aida to search for an event with name opera aida. The curly braces in our path alert the Gorilla mux package that SearchCriteria and search are basically variables expected to be substituted in the real-incoming HTTP request URL with other things.

The Gorilla mux package enjoys powerful support for URL path variables. It also supports pattern-matching via regular expressions. So, for example, if I use a path that looks like /{search:[0-9]+}, it will provide me a variable called search that hosts a number.

After we finish defining our routers, paths, and handlers, we will need to specify the local TCP address where our web server will listen for incoming HTTP requests. For this, we need Go's net/http package; here's what the code would look like:

http.ListenAndServe(":8181", r)

In this single line of code, we created a web server. It will listen for incoming HTTP requests on local port 8181 and will use the r object as the router for the requests. We created the r object earlier using the mux package.

It's now time to put all the code we covered up to this point together. Let's assume that the code lives inside a function called ServeAPI() that is responsible for activating the Restful API logic for our microservice.

func ServeAPI(endpoint string) error {
handler := &eventservicehandler{}
r := mux.NewRouter()
eventsrouter := r.PathPrefix("/events").Subrouter()
eventsrouter.Methods("GET").Path("/{SearchCriteria}/{search}").HandlerFunc(handler.FindEventHandler)
eventsrouter.Methods("GET").Path("").HandlerFunc(handler.AllEventHandler)
eventsrouter.Methods("POST").Path("").HandlerFunc(handler.NewEventHandler)
return http.ListenAndServe(endpoint, r)
}

We defined the eventServiceHandler object to look like this:

type eventServiceHandler struct {}

func (eh *eventServiceHandler) findEventHandler(w http.ResponseWriter, r *http.Request) {}

func (eh *eventServiceHandler) allEventHandler(w http.ResponseWriter, r *http.Request) {}

func (eh *eventServiceHandler) newEventHandler(w http.ResponseWriter, r *http.Request) {}

Obviously, the next step will be to fill in the empty methods of the eventServiceHandler type. We have the findEventHandler(), allEventHandler(), and newEventHandler() methods. Each one of them needs a persistence layer to carry out their tasks. That is because they either retrieve stored data or add new data to a store.

As mentioned earlier in this section, the persistence layer is the component of a microservice that is tasked with storing data in databases or retrieving data from databases. We arrived to the point where we need to cover the persistence layer in much more detail.