- Cloud Native programming with Golang
- Mina Andrawos Martin Helmich
- 1906字
- 2025-04-04 17:38:11
Implementing our RESTful APIs handler functions
So, now that have covered our persistence layer, it's time to return to our RESTful API handlers and cover their implementation. Earlier in this chapter, we defined the eventServiceHandler struct type 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) {}
The eventServiceHandler type now needs to support the DatabaseHandler interface type we created earlier in the chapter in order to be capable of performing database operations. This will make the struct look like this:
type eventServiceHandler struct {
dbhandler persistence.DatabaseHandler
}
Next, we will need to write a constructor to initialize the eventServiceHandler object; it will look as follows:
func newEventHandler(databasehandler persistence.DatabaseHandler) *eventServiceHandler {
return &eventServiceHandler{
dbhandler: databasehandler,
}
}
However, we left the three methods of the eventServiceHandler struct type empty. Let's go through them one by one.
The first method findEventHandler() is responsible for handling HTTP requests used to query events stored in our database. We can query events via their IDs or names. As mentioned earlier in the chapter, when searching for an ID, the request URL will resemble /events/id/3434 and will be of the GET type. On the other hand, when searching by name, the request will resemble /events/name/jazz_concert and be of the GET type. As a reminder, the following is how we defined the path and linked it to the handler:
eventsrouter := r.PathPrefix("/events").Subrouter()
eventsrouter.Methods("GET").Path("/{SearchCriteria}/{search}").HandlerFunc(handler.findEventHandler)
{SearchCriteria} and {Search} are two variables in our path. {SearchCriteria} can be replaced with id or name.
Here is what the code for the findEventHandler method will look like:
func (eh *eventServiceHandler) findEventHandler(w http.ResponseWriter, r *http.Request) {
vars := mux.Vars(r)
criteria, ok := vars["SearchCriteria"]
if !ok {
w.WriteHeader(400)
fmt.Fprint(w, `{error: No search criteria found, you can either search by id via /id/4
to search by name via /name/coldplayconcert}`)
return
}
searchkey, ok := vars["search"]
if !ok {
w.WriteHeader(400)
fmt.Fprint(w, `{error: No search keys found, you can either search by id via /id/4
to search by name via /name/coldplayconcert}`)
return
}
var event persistence.Event
var err error
switch strings.ToLower(criteria) {
case "name":
event, err = eh.dbhandler.FindEventByName(searchkey)
case "id":
id, err := hex.DecodeString(searchkey)
if err == nil {
event, err = eh.dbhandler.FindEvent(id)
}
}
if err != nil {
fmt.Fprintf(w, "{error %s}", err)
return
}
w.Header().Set("Content-Type", "application/json;charset=utf8")
json.NewEncoder(w).Encode(&event)
}
The method takes two arguments: an object of the http.ResponseWriter type, which represents the HTTP response we need to fill, whereas the second argument is of the *http.Request type, which represents the HTTP request that we received. In the first line, we use mux.Vars() with the request object as an argument; this will return a map of keys and values, which will represent our request URL variables and their values. So, for example, if the request URL looks like /events/name/jazz_concert, we will have two key-value pairs in our resulting map—the first key is "SearchCriteria" with a value of "name", whereas the second key is "search" with a value of jazz_concert. The resulting map is stored in the vars variable.
We then obtain the criteria from our map in the next line:
criteria, ok := vars["SearchCriteria"]
So, the criteria variable will now have either name or id if the user sent the correct request URL. The ok variable is of the boolean type; if ok is true, then we will find a key called SearchCriteria in our vars map. If it is false, then we know that the request URL we received is not valid.
Next, we check whether we retrieved the search criteria; if we didn't, then we report the error and then exit. Notice here how we report the error in a JSON like format? That is because it is typically preferred for RESTful APIs with JSON body formats to return everything in JSON form, including errors. Another way to do this is to create a JSONError type and feed it our error strings; however, I will just spell out the JSON string here in the code for simplicity:
if !ok {
fmt.Fprint(w, `{error: No search criteria found, you can either search by id via /id/4 to search by name via /name/coldplayconcert}`)
return
}
fmt.Fprint allows us to write the error message directly to the w variable, which contains our HTTP response writer. The http.responseWriter object type supports Go's io.Writer interface, which can be used with fmt.Fprint().
Now, we will need to do the same with the {search} variable:
searchkey, ok := vars["search"]
if !ok {
fmt.Fprint(w, `{error: No search keys found, you can either search by id via /id/4
to search by name via /name/coldplayconcert}`)
return
}
It's time to extract the information from the database based on the provided request URL variables; here is how we do it:
var event persistence.Event
var err error
switch strings.ToLower(criteria) {
case "name":
event, err = eh.dbhandler.FindEventByName(searchkey)
case "id":
id, err := hex.DecodeString(searchkey)
if nil == err {
event, err = eh.dbhandler.FindEvent(id)
}
}
In case of the name search criteria, we will use the FindEventByName() database handler method to search by name. In case of the ID search criteria, we will convert the search key to a slice of bytes using hex.DecodeString()—if we successfully obtain the slice of bytes, we will call FindEvent() with the obtained ID.
We then check whether any errors occurred during the database operations by checking the err object. If we find errors, we write a 404 error header in our response, then print the error in the HTTP response body:
if err != nil {
w.WriteHeader(404)
fmt.Fprintf(w, "Error occured %s", err)
return
}
The last thing we need to do is to convert the response to a JSON format, so we change the HTTP content-type header to application/json; then, we use the powerful Go JSON package to convert the results obtained from our database calls to the JSON format:
w.Header().Set("Content-Type", "application/json;charset=utf8")
json.NewEncoder(w).Encode(&event)
Now, let's look at the code for the allEventHandler() method, which will return all the available events in the HTTP response:
func (eh *eventServiceHandler) allEventHandler(w http.ResponseWriter, r *http.Request) {
events, err := eh.dbhandler.FindAllAvailableEvents()
if err != nil {
w.WriteHeader(500)
fmt.Fprintf(w, "{error: Error occured while trying to find all available events %s}", err)
return
}
w.Header().Set("Content-Type", "application/json;charset=utf8")
err = json.NewEncoder(w).Encode(&events)
if err != nil {
w.WriteHeader(500)
fmt.Fprintf(w, "{error: Error occured while trying encode events to JSON %s}", err)
}
}
We start by calling the FindAllAvailableEvents() that belongs to the database handler in order to obtain all events from the database. We then check whether any errors occurred. If any found, we write an error header, print the error to the HTTP response, and then return from the function.
If no errors have occurred, we write application/json to the Content-Type header of the HTTP response. We then encode the events to the JSON format and send them to the HTTP response writer object. Again, if any errors occur, we will log them and then exit.
Now, let's discuss the newEventHandler() handler method, which will add a new event to our database using the data retrieved from incoming HTTP requests. We expect the event data in the incoming HTTP request to be in the JSON format. Here is what the code will look like:
func (eh *eventServiceHandler) newEventHandler(w http.ResponseWriter, r *http.Request) {
event := persistence.Event{}
err := json.NewDecoder(r.Body).Decode(&event)
if err != nil {
w.WriteHeader(500)
fmt.Fprintf(w, "{error: error occured while decoding event data %s}", err)
return
}
id, err := eh.dbhandler.AddEvent(event)
if nil != err {
w.WriteHeader(500)
fmt.Fprintf(w, "{error: error occured while persisting event %d %s}",id, err)
return
}
In the first line, we create a new object of the persistence.Event type, which we will use to hold the data we are expecting to parse out from the incoming HTTP request.
In the second line, we use Go's JSON package to take the body of the incoming HTTP request (which we obtain by calling r.Body). We then decode the JSON data embedded in it and feed it to the new event object, as follows:
err := json.NewDecoder(r.Body).Decode(&event)
We then check our errors as usual. If no errors are observed, we call the AddEvent() method of our database handler and pass the event object as the argument. This in effect will add the event object we obtained from the incoming HTTP request to the database. We then check errors again as usual and exit.
To put the final touches on our events microservice, we will need to do three things. The first is to allow the ServeAPI() function we covered earlier in this chapter, which define the HTTP routes and handlers, to call the eventServiceHandler constructor. The code will end up looking like this:
func ServeAPI(endpoint string, dbHandler persistence.DatabaseHandler) error {
handler := newEventHandler(dbHandler)
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)
}
The second final touch we need to do is to write a configuration layer for our microservice. As mentioned earlier in the chapter, a well-designed microservice needs a configuration layer which reads from a file, a database, an environmental variable, or a similar medium. There are three main parameters we need to support for now for our configuration—the database type used by our microservice (MongoDB is our default), the database connection string (default is mongodb://127.0.0.1 for a local connection), and the Restful API endpoint. Here is what our configuration layer will end up looking like:
package configuration
var (
DBTypeDefault = dblayer.DBTYPE("mongodb")
DBConnectionDefault = "mongodb://127.0.0.1"
RestfulEPDefault = "localhost:8181"
)
type ServiceConfig struct {
Databasetype dblayer.DBTYPE `json:"databasetype"`
DBConnection string `json:"dbconnection"`
RestfulEndpoint string `json:"restfulapi_endpoint"`
}
func ExtractConfiguration(filename string) (ServiceConfig, error) {
conf := ServiceConfig{
DBTypeDefault,
DBConnectionDefault,
RestfulEPDefault,
}
file, err := os.Open(filename)
if err != nil {
fmt.Println("Configuration file not found. Continuing with default values.")
return conf, err
}
err = json.NewDecoder(file).Decode(&conf)
return conf,err
}
The third touch is to build a database layer package that acts as the gateway to the persistence layer in our microservice. The package will utilize the factory design pattern by implementing a factory function. A factory function will manufacture our database handler. This is done by taking the name of the database that we would like to connect to, as well as the connection string, then returning a database handler object which we can use for database related tasks from this point forward. We currently only support MongoDB, so here is how this would look like:
package dblayer
import (
"gocloudprogramming/chapter2/myevents/src/lib/persistence"
"gocloudprogramming/chapter2/myevents/src/lib/persistence/mongolayer"
)
type DBTYPE string
const (
MONGODB DBTYPE = "mongodb"
DYNAMODB DBTYPE = "dynamodb"
)
func NewPersistenceLayer(options DBTYPE, connection string) (persistence.DatabaseHandler, error) {
switch options {
case MONGODB:
return mongolayer.NewMongoDBLayer(connection)
}
return nil, nil
}
The fourth and final touch is our main package. We will write the main function that makes use of the flag package to take the location of the configuration file from the user and then use the configuration file to initialize the database connection and the HTTP server. The following is the resultant code:
package main
func main(){
confPath := flag.String("conf", `.\configuration\config.json`, "flag to set
the path to the configuration json file")
flag.Parse()
//extract configuration
config, _ := configuration.ExtractConfiguration(*confPath)
fmt.Println("Connecting to database")
dbhandler, _ := dblayer.NewPersistenceLayer(config.Databasetype, config.DBConnection)
//RESTful API start
log.Fatal(rest.ServeAPI(config.RestfulEndpoint, dbhandler, eventEmitter))
}
With this piece of code, we come to the conclusion of this chapter. In the next chapter, we will discuss how to secure our microservice.