Building a lightweight high-performance RESTful API using HTTP router in Go

In this article we are going to show you how to build a high-performance and highly efficient REST API web service application. Let’s take a look on how to build REST API web service in Go. The standard http Go package has the ability to handle both the HTTP client and server tasks. The problem with this package is that its routing and multiplexing is pretty basic. Depending on your need it might require some extra code which can lead to boilerplate routing codes. If you want to avoid this, there are several packages that provided solutions. Here are two of the few packages that can be used:

  • Gorilla/mux which is portion of the entire Gorilla web toolkit. The mux package provides flexible set of standards on which each request can match with and that includes host, -schemes, HTTP headers and more.
  • Pat is a package that is based upon the idea of the ruby package Sinatra. The purpose is to make it more readable. Hence, the implementation contains named parameters which makes it easy to read.

For this article we will be focusing more on efficiency and performance of a RESTful API webservice. The Julienschmidt http router package focus more on that to minimize memory usage and low routing handling time. This is why it is considered to be a fast routing package. This package feature case-insensitive paths, removing unnecessary invalid characters in a path and handling optional trailing (e.g: /.). Hence, this package will be used to build our high performing and efficient RESTful api.

  • Import julienschmidt package

First, make sure your GOPATH is set accordingly as describe in these articles:

– For MACOS user

– For Windows user

– For Linux user

Open the terminal and execute the following command:

go get github.com/julienschmidt/httprouter

  • Write webservice application

After importing the package, we can start writing some codes. For this example, we are just going to write a simple web service that return a simple JSON response. The context of the web service application entails endpoints for retrieve, insert, modify and remove students. The list represents a dummy database since we want to keep it as simple as possible.

We will start by creating the necessary struct (models) required for this web service app

Student struct

This struct contains the following data fields: 

ID -> The dummy student ID (of type int)

Name -> The student name (of type string)

Age -> The student age (of type int)

type student struct {
   ID   int    `json:"id"`
   Name string `json:"name"`
   Age  int    `json:"age"`
}

Response Message struct

The response message struct will be used as a generic struct for sending response message back to the client and it contains only one data field: 

Message -> The server response message (of type string)

type responseMessage struct {
   Message string `json:"message"`
}

Student list (a dummy representation of a database)

This list will simulate a dummy database for the insertion, deletion, modification and removal of students.

var studentList []student

ID counter (counter for incrementing student ID when inserted)

This counter will be incremented each time a new student is inserted in the list. In context of a database, it can be student ID or the database primary key (which can be automatically incremented each time a new item is inserted). 

var idCounter = 0

Implement some crucial function to keep our code as clean as possible

In Go, sending a client response and error messages is a standard procedure. To avoid code duplication, we are going to implement some crucial functions that can be re-use. These functions are: 

  • Mapped as json

This function will set the response header as json which is important so that the client knows that the response is in the form of a json.

func mappedAsJson(responseWriter http.ResponseWriter) {
   responseWriter.Header().Set("Content-Type", "application/json")
} 
  • Bad request response

This is the default bad request response that will be used in this web service application.

func BadRequestResponse(response http.ResponseWriter, message string) {

   mappedAsJson(response)
   response.WriteHeader(http.StatusBadRequest)
   errorResponse := responseMessage{Message: message}
   errorResponseJson, marshalError := json.Marshal(errorResponse)

   if marshalError != nil {
      _, _ = response.Write([]byte(""))
   } else {
      _, _ = response.Write(errorResponseJson)
   }
}

  • Send internal server error

This function is almost the same as the previous one, but in this case it has to do with the internal server error response. It will be used when thing goes totally wrong on the server side.

func sendInternalServerError(response http.ResponseWriter) {

   mappedAsJson(response)
   response.WriteHeader(http.StatusInternalServerError)
   errorResponse := responseMessage{Message: "Unable to handle request"}
   errorResponseJson, marshalError := json.Marshal(errorResponse)

   if marshalError != nil {
      _, _ = response.Write([]byte(""))
   } else {
      _, _ = response.Write(errorResponseJson)
   }
}
  • Send response message

This function handles with responses that contain a specific message to the client. Such message can be an error message or if you want to inform the client about something (e.g: the list of student is empty etc…).

func sendResponseMessage(statusCode int, response http.ResponseWriter, message responseMessage) {

   if statusCode != 0 && response != nil {
      mappedAsJson(response)
      response.WriteHeader(statusCode)
      jsonResponse, jsonMarshalError := json.Marshal(message)
      if jsonMarshalError != nil {
         sendInternalServerError(response)
      } else {
         _, jsonResponseError := response.Write(jsonResponse)
         if jsonResponseError != nil {
            sendInternalServerError(response)
         }
      }
   }
}
  • Send response

Like the previous function, this one handles with responses that contains student information. In most cases, it will be used when a request has succeed.

func sendResponse(statusCode int, response http.ResponseWriter, student student) {

   if statusCode != 0 && response != nil {
      mappedAsJson(response)
      response.WriteHeader(statusCode)
      jsonResponse, jsonMarshalError := json.Marshal(student)
      if jsonMarshalError != nil {
         sendInternalServerError(response)
      } else {
         _, jsonResponseError := response.Write(jsonResponse)
         if jsonResponseError != nil {
            sendInternalServerError(response)
         }
      }
   }
}
  • List contains record by id

This function check if the student list (dummy database in our case) contains a specific student based on the student ID provided. Such ID has been generated by the counter variable during the insertion of the student.

func listContainsRecordByID(id int) bool {

   containsData := false

   for _, studentFound := range studentList {
      if studentFound.ID == id {
         containsData = true
      }
   }

   return containsData
}
  • List contains record

This function check if the list contains a student record based on the name and age provided. As you can see it return multiple variables, for more information about multiple return functions and how to handle its values check this article out.

func listContainsRecord(name string, age int) (bool, student) {

   containsData := false
   var student student

   for _, studentFound := range studentList {
      if strings.ToLower(studentFound.Name) == strings.ToLower(name) && studentFound.Age == age {
         containsData = true
         student = studentFound
      }
   }

   return containsData, student
}

In this case it returns a boolean and student struct

Server Endpoints

In the following section, we will be implementing the server endpoints and also initiating and linking endpoints to http router.

  • Initiate the router instance 

To start using the http router package create a new instance in the main function.

func main() {
   router := httprouter.New()
}
  • Get all students endpoint 

Let start with the get all student endpoint. This endpoint will return the list of students that has been inserted into the list. The path to this endpoint will be ‘/student/all’ and it is a GET request.

“GET: /student/all”

First let start by define it on the newly created router instance:

func main() {
   router := httprouter.New()
   router.GET("/student/all", handleGetAllStudent)
} 

Next, let implement the handler function for this endpoint:

func handleGetAllStudent(response http.ResponseWriter, _ *http.Request, _ httprouter.Params) {

   mappedAsJson(response)
   response.WriteHeader(http.StatusOK)

   if len(studentList) == 0 {
      marshalledResponse, _ := json.Marshal([]student{})
      _, writerError := response.Write(marshalledResponse)
      if writerError != nil {
         writeInternalServerError(response)
      }
   } else {
      marshalledResponse, _ := json.Marshal(studentList)
      _, responseWriteError := response.Write(marshalledResponse)
      if responseWriteError != nil {
         writeInternalServerError(response)
      }
   }
}
  • Get student endpoint

With this endpoint you can retrieve a specific student from the list based on the ID provided. The response is a json object containing student information otherwise a not found response will be send with a generic message. The path for this endpoint is ‘/student’ and it is a GET request. The student ID must be provided in the request parameter.

“GET: /student”
func handleGetStudent(response http.ResponseWriter, request *http.Request, _ httprouter.Params) {

   params := request.URL.Query()

   if params == nil || len(params) == 0 || params["id"] == nil || params["id"][0] == "" {
      BadRequestResponse(response, "Invalid ID")
      return
   }

   studentID := params["id"][0]

   studentIdInt, err := strconv.Atoi(studentID)
   if err != nil {
      BadRequestResponse(response, "Invalid ID")
      return
   }

   var requestedStudent = student{
      ID:   -1,
      Name: "",
      Age:  -1,
   }

   for _, studentFound := range studentList {

      if studentFound.ID == studentIdInt {
         requestedStudent = studentFound
      }
   }

   if requestedStudent.ID == -1 {
      sendResponseMessage(http.StatusNotFound, response, responseMessage{Message: "Record not found"})
   } else {
      sendResponse(http.StatusOK, response, requestedStudent)
   }
}
  • Add student endpoint

Adding students to the list is the purpose of this endpoint. In order to add student, the required data must be provided in the request body. In this case, the name and age. The response is a json object containing the newly inserted student information (including the record ID). As for the path for this endpoint, we will be using ‘/student’ and it is a POST request.

“POST: /student”
func addStudent(response http.ResponseWriter, request *http.Request, _ httprouter.Params) {

   var newStudent student
   parseError := json.NewDecoder(request.Body).Decode(&newStudent)

   if parseError != nil {
      if parseError == io.EOF {
         BadRequestResponse(response, "Incomplete data")
      } else {
         sendInternalServerError(response)
      }
      return
   } else if newStudent.Name == "" || newStudent.Age == 0 {
      BadRequestResponse(response, "Missing data")
      return
   }

   listContainsData, studentFound := listContainsRecord(newStudent.Name, newStudent.Age)

   if listContainsData {
      sendResponse(http.StatusOK, response, studentFound)
   } else {
      idCounter++

      studentToAdd := student{
         ID:   idCounter,
         Name: newStudent.Name,
         Age:  newStudent.Age,
      }
      studentList = append(studentList, studentToAdd)
      sendResponse(http.StatusOK, response, studentToAdd)
   }
}
  • Remove student endpoint

This endpoint is for removing student from the list. In this case only the student ID must be provided as part of the request parameter. The path for this endpoint ‘/student’ and it is a DELETE request.

“DELETE: /student”
func handleRemoveStudent(response http.ResponseWriter, request *http.Request, _ httprouter.Params) {

   params := request.URL.Query()

   if params == nil || len(params) == 0 || params["id"] == nil || params["id"][0] == "" {
      BadRequestResponse(response, "Invalid ID")
      return
   }

   studentID := params["id"][0]

   studentIdInt, err := strconv.Atoi(studentID)
   if err != nil {
      BadRequestResponse(response, "Invalid ID")
      return
   }

   for index, studentFound := range studentList {
      if studentFound.ID == studentIdInt {
         studentList = append(studentList[:index], studentList[index+1:]...)
      }
   }
   sendResponseMessage(http.StatusOK, response, responseMessage{Message: "Student removed"})
}
  • Update student endpoint

Lastly, this endpoint is intended for updating student information. In this case, the student ID must be provided as part of the request parameter. The newly updated user information will be part of the request body. The path for this endpoint: ‘/student/update’ and it is a PATCH request.

“PATCH: /student/update”
func updateStudent(response http.ResponseWriter, request *http.Request, _ httprouter.Params) {

   params := request.URL.Query()

   if params == nil || len(params) == 0 || params["id"] == nil || params["id"][0] == "" {
      BadRequestResponse(response, "Invalid ID")
      return
   }

   studentID := params["id"][0]

   studentIdInt, err := strconv.Atoi(studentID)
   if err != nil {
      BadRequestResponse(response, "Invalid ID")
      return
   }

   listContainsData := listContainsRecordByID(studentIdInt)

   var updatedStudent student
   parseError := json.NewDecoder(request.Body).Decode(&updatedStudent)

   if !listContainsData {
      sendResponseMessage(http.StatusNotFound, response, responseMessage{Message: "Record not found!"})
      return
   }

   if parseError != nil {
      if parseError == io.EOF {
         BadRequestResponse(response, "Incomplete data")
      } else {
         sendInternalServerError(response)
      }
      return
   }

   for index, studentFound := range studentList {

      if studentFound.ID == studentIdInt {

         studentToUpdate := &studentList[index] // point to the address of the item

         if updatedStudent.Name != "" {
            studentToUpdate.Name = updatedStudent.Name
         }

         if updatedStudent.Age != 0 {
            studentToUpdate.Age = updatedStudent.Age
         }

         updatedStudent = *studentToUpdate
      }
   }

   sendResponse(http.StatusOK, response, updatedStudent)
}
  • Create main function and router instance 

After implementing all the endpoints, you need to link them to the router in order to start hosting and listen for incoming request from clients. That can be done by implementing the following in the main function: 

func main() {

   router := httprouter.New()
   router.GET("/student/all", handleGetAllStudent)
   router.GET("/student", handleGetStudent)
   router.PATCH("/student/update", updateStudent)
   router.POST("/student", addStudent)
   router.DELETE("/student", handleRemoveStudent)

   serverError := http.ListenAndServe(":3000", router)
   if serverError != nil {
      log.Fatal("Unable to start web server, cause: ", serverError)
   }
}

Run and test simple webservice

In order to test the webservice application, we are going to use postman to make all the requests and to display the response. If you don’t have postman installed yet, download it from their official site

In order to start the server, you need to set the GOPATH. In case you want more info about setting GOPATH, check these articles out:

– For MACOS user

– For Windows user

– For Linux user

To start the server, navigate to the project and execute the following command:

go run efficient_high_performance_webservice.go

In this example, our go file name is efficient_high_performance_webservice.go.

  • Add student
  • Get student
  • Get all students
  • Update student
  • Remove student

Visit our GitHub repository for the project source code and clone our repository for all future projects.

Follow us:

Leave a Reply

Your email address will not be published. Required fields are marked *