Let’s Go, Everything you need to know about creating a RESTful Api in Go — Part II

Supun Muthutantrige
9 min readApr 19, 2019

--

Detailed overview on how to make the Go endpoints RESTful

PART II

In the Previous article (PART I), we created an endpoint which didn’t do much but printed some stuff in the server console. The code snippet we used to start the serve and accept request is as follows,

[app.go]package mainimport (
"fmt"
"log"
"net/http"
)
func main() {
http.HandleFunc("/", homePageHandler)
log.Fatal(http.ListenAndServe(":8080", nil))
}
func homePageHandler(w http.ResponseWriter, r *http.Request) {
fmt.Println("Request came through to Home Page")
}

Now let’s say we want expose an endpoint other than a http “GET”. In the above code base, the handler function could only accept GET methods. If we are to change this and allow other http methods, we need to do something like the following,

[app.go]package mainimport (
"fmt"
"log"
"net/http"
)
func main() {
http.HandleFunc("/", homePageHandler)
log.Fatal(http.ListenAndServe(":8080", nil))
}
func homePageHandler(w http.ResponseWriter, r *http.Request) {
if r.METHOD == 'POST' {
// post handler
} else if r.r.METHOD == 'GET'{
// get handler
}
fmt.Println("Request came through to Home Page")
}

As you can see, identifying and dedicating to handlers in this way seems less extensible and ugly. For this purpose there is a better 3rd party router and a dispatcher that we could make use of.

gorilla/mux to handle routing

This package implements a request router and a dispatcher, which routes the incoming requests to a handler based on the matching url. To install the package type,

go get github.com/gorilla/mux

Now let’s improve the above code to support multiple http methods.

[app.go]package mainimport (
"fmt"
"log"
"net/http"
"github.com/gorilla/mux"
)
func main() {
r := mux.NewRouter()
r.HandleFunc("/", homePageHandler).Methods("GET")
r.HandleFunc("/", homePageHandlerPost).Methods("POST")

log.Fatal(http.ListenAndServe(":8080", r))
}
func homePageHandler(w http.ResponseWriter, r *http.Request) {
fmt.Println("Request came through to Home Page Get method")
}
func homePageHandlerPost(w http.ResponseWriter, r *http.Request) {
fmt.Println("Request came through to Home Page Post method")
}

When you run the application and execute POST and GET calls subsequently, you will get the following output, which shows that the router is working as expected.

Let’s rethink our scenario for a better API spec

Let’s consider an Account flow and come up with a proper API spec. Our api will do following tasks,

  • POST /accounts: create an account
  • GET /accounts/{id}: return an account by account id
  • DELETE /account/{id}: delete an account by account id

Now, will change our code to support above functionalities.

[app.go]package mainimport (
"log"
"net/http"
"github.com/gorilla/mux"
)
func main() {
r := mux.NewRouter()
r.HandleFunc("/accounts", createAccountHandler).Methods("POST")
r.HandleFunc("/accounts/{id}", getAccountHandler).Methods("GET")
r.HandleFunc("/accounts/{id}", deleteAccountHandler)
.Methods("DELETE")
log.Fatal(http.ListenAndServe(":8080", r))
}
func createAccountHandler(w http.ResponseWriter, r *http.Request) {
log.Print("Request received to create an Account")
}
func getAccountHandler(w http.ResponseWriter, r *http.Request) {
log.Print("Request received to get an Account by account id")
}
func deleteAccountHandler(w http.ResponseWriter, r *http.Request) {
log.Print("Request received to delete an Account by account id")
}

I have updated the fmt.Println function with log.Print, changed the handler methods and updated the routers as well. When you run the server and trigger above endpoints separately via Postman, following logs can be seen in the server.

createAccountHandler

This handler should expect an Account request json payload, and persist the account. Persisting the account can be to a database or for the scope of this tutorial will be kept in-memory. The structure of the Account payload is as follows.

[Request Payload]Account {
Number id
String firstName
String lastName
String userName
}

Let’s change our code base to accept a json payload as above and save it in memory, so that when we implement getAccountHandler, we could fetch in-memory saved accounts.

A class in object-oriented paradigm is somewhat similar to a struct in Go but Go doesn’t allow inheritance but support composition (read more about structs - source). We should create an Account struct to hold account related information.

[Request Payload]Account {
Number id
String firstName
String lastName
String userName
}
[Equivalent Account struct]type Account struct {
id int8 `json:"ref"`
firstName string `json:"first_name"`
lastName string `json:"last_name"`
userName string `json:"user_name"`
}

Note that json tags for each field (`json:”first_name”`, `json:”last_name”`, etc.) specifies how these fields will appear in the Account json request payload (read more — source).

I have updated the createAccountHandler function with the following implementation details,

import (
"encoding/json"
...
)
var accountMap map[string]Accountfunc init() {
accountMap = make(map[string]Account)
}
...func createAccountHandler(w http.ResponseWriter, r *http.Request){
log.Print("Request received to create an Account")
var account Account
json.NewDecoder(r.Body).Decode(&account)
id := account.ID
accountMap[id] = account
log.Print("Added the Account ", account, " to list of accounts ",
accountMap)
}
...

Here I have imported go built-in json encoding decoding package to map Account json request payload to Account struct.

var account Account
json.NewDecoder(r.Body).Decode(&account)

var account Account initialize a new memory location to hold Account data. In the next line, a Decoder reads a json payload and (decodes) converts it into a go struct. Here NewDecoder(r.Body), returns a new decoder that reads from the incoming request payload. On the other hand, func Decode reads the next json-encoded value, decodes it and stores it in the value pointed to (in this instance to account pointer).

So the line, json.NewDecoder(r.Body).Decode(&account) reads a json payload, decodes it and store it in the value pointed in (here &account refers to a memory address of account).

The idea behind creating a map (key: account id, value: account struct) is to be able to fetch our accounts fast. Since we are not using any databases as of now, we’ll store our data in-memory and retrieve in other operations such as getAccountHandler. I have only extracted the required code snippets which deals with initializing the map and populating it.

...
var accountMap map[string]Account
func init() {
accountMap = make(map[string]Account)
}
...
func createAccountHandler(w http.ResponseWriter, r *http.Request){
...
id := account.ID
accountMap[id] = account
...
}

Line, var accountMap map[string]Account, declares a map which contains a string as its key and an Account struct as its value. Inside init() function, we initialize the account map using make(map[string]Account) else the map will be empty, so we won’t be able to populate it with data.

“The make function allocates and initializes a hash map data structure and returns a map value that points to it”. — source

Line, id := account.ID, gets the incoming account id and assigns to String variable, id. Then, accountMap[id] = account, assigns the account struct to accountMap where the key: id and value account.

In order for our createAccountHandler function to be completed as a proper RESTful endpoint, we need to add the proper response headers and status code. Let’s update our code,

func createAccountHandler(w http.ResponseWriter, r *http.Request){
...
log.Print("Added the Account ", account, " to list of accounts ",
accountMap)
w.Header().Add("Content-Type", "application/json")
w.WriteHeader(http.StatusCreated)
json.NewEncoder(w).Encode(account)
}

Up until now we didn’t use the w (http.ResponseWriter interface, read more — source). Line w.Header().Add(“Content-Type”, “application/json”), adds a response header and w.WriteHeader(http.StatusCreated), specifies the intended status code as 201 Created since this is a POST request which creates a resource.

In line json.NewEncoder(w).Encode(account), json.NewEncoder(w) create a new encoder which writes to w. Our intention is to prepare the response by specifying proper headers, status code and if required return the created payload as the response body. Line json.NewEncoder(w) will return the response header and status code. If we don’t want to return any response payload as part of the response body, only this line is sufficient. Whereas line, Encode(account), converts the account struct to a json payload and returns as part of the response along with header and status code.

The entire code so far is as follows,

[app.go]package mainimport (
"encoding/json"
"log"
"net/http"
"github.com/gorilla/mux"
)
//Account Json request payload is as follows,
//{
// "id": "1",
// "first_name": "james",
// "last_name": "bolt",
// "user_name": "james1234"
//}
type Account struct {
ID string `json:"id"`
FirstName string `json:"first_name"`
LastName string `json:"last_name"`
UserName string `json:"user_name"`
}
var accountMap map[string]Accountfunc init() {
accountMap = make(map[string]Account)
}
func main() {
r := mux.NewRouter()
r.HandleFunc("/accounts", createAccountHandler).Methods("POST")
r.HandleFunc("/accounts/{id}", getAccountHandler).Methods("GET")
r.HandleFunc("/accounts/{id}",deleteAccountHandler)
.Methods("DELETE)
log.Fatal(http.ListenAndServe(":8080", r))
}
func createAccountHandler(w http.ResponseWriter, r *http.Request) {
log.Print("Request received to create an Account")

var account Account
json.NewDecoder(r.Body).Decode(&account)
id := account.ID
accountMap[id] = account
log.Print("Successfully created the Account ", account)
w.Header().Add("Content-Type", "application/json")
w.WriteHeader(http.StatusCreated)
json.NewEncoder(w).Encode(account)
}
func getAccountHandler(w http.ResponseWriter, r *http.Request) {
log.Print("Request received to get an Account by account id")
}
func deleteAccountHandler(w http.ResponseWriter, r *http.Request) {
log.Print("Request received to delete an Account by account id")
}

Will execute postman and see what we get,

Postman

POST request and response

Server Logs

Server logs

As you can see, running createAccountHandler POST method returns the intended response body along with 201 status code and headers.

getAccountHandler

Steps we had to go though when implementing createAccountHandler was lengthy. Now we have to get whatever stored accounts. Since we created a map to store created account, this section is all about accessing that map and return the account if found.

...func getAccountHandler(w http.ResponseWriter, r *http.Request) {
params := mux.Vars(r)
id := params["id"]
log.Print("Request received to get an account by account id: "
,id)
account, key := accountMap[id]
w.Header().Add("Content-Type", "application/json")
if key {
log.Print("Successfully retrieved the account ", account, " for
account id: ", id)
w.WriteHeader(http.StatusOK)
json.NewEncoder(w).Encode(account)
} else {
log.Print("Requested account is not found for account id: ",id)
w.WriteHeader(http.StatusNotFound)
json.NewEncoder(w)
}
}
...

Following code snippet, extracts the path variable in incoming URI <host>/accounts/1 and assigns it to id in <host>/accounts/{id}.

params := mux.Vars(r)
id := params["id"]

Line, account, key := accountMap[id], access the account map with the id specified in the incoming URL. Here, account in {account, key} gets the account struct stored in the accountMap and key gets a true or false value depending on whether the key is found or not.

The rest of the code in getAccountHandler is to return account with status code 200 OK if found in the map, or 404 Not Found if not found in the map.

Postman

  • When account is found
200 OK
  • When account is not found
404 Not Found

deleteAccountHandler

The final function is to remove a resource from the list of accounts if found. Similar to what we did in createAccountHandler and getAccountHandler, you can complete implementing deleteAccountHandler on your own.

What we have implemented so far is as follows,

[app.go]package mainimport (
"encoding/json"
"log"
"net/http"
"github.com/gorilla/mux"
)
//Account Json request payload is as follows,
//{
// "id": "1",
// "first_name": "james",
// "last_name": "bolt",
// "user_name": "james1234"
//}
type Account struct {
ID string `json:"id"`
FirstName string `json:"first_name"`
LastName string `json:"last_name"`
UserName string `json:"user_name"`
}
var accountMap map[string]Accountfunc init() {
accountMap = make(map[string]Account)
}
func main() {
r := mux.NewRouter()
r.HandleFunc("/accounts", createAccountHandler).Methods("POST")
r.HandleFunc("/accounts/{id}", getAccountHandler).Methods("GET")
r.HandleFunc("/accounts/{id}",deleteAccountHandler)
.Methods("DELETE)
log.Fatal(http.ListenAndServe(":8080", r))
}
func createAccountHandler(w http.ResponseWriter, r *http.Request) {
log.Print("Request received to create an Account")

var account Account
json.NewDecoder(r.Body).Decode(&account)
id := account.ID
accountMap[id] = account
log.Print("Successfully created the Account ", account)
w.Header().Add("Content-Type", "application/json")
w.WriteHeader(http.StatusCreated)
json.NewEncoder(w).Encode(account)
}
func getAccountHandler(w http.ResponseWriter, r *http.Request) {
params := mux.Vars(r)
id := params["id"]
log.Print("Request received to get an account by account id: "
,id)
account, key := accountMap[id]
w.Header().Add("Content-Type", "application/json")
if key {
log.Print("Successfully retrieved the account ", account, " for
account id: ", id)
w.WriteHeader(http.StatusOK)
json.NewEncoder(w).Encode(account)
} else {
log.Print("Requested account is not found for account id: ",id)
w.WriteHeader(http.StatusNotFound)
json.NewEncoder(w)
}
}
func deleteAccountHandler(w http.ResponseWriter, r *http.Request) {
log.Print("Request received to delete an Account by account id")
//add your own flavor to this function :)
}

So, this is the end of a lengthy article, Next will see how to expose the API spec via swagger.

check my personal blog to view more — icodeforpizza

Prev Section — Part I

Next Section — Part III

--

--