Build a REST API in Golang


golang rest api

This post explains how to develop a simple REST API in Golang, by building a CRUD(Create/Read/Update/Delete) service for managing orders. I intended to keep this post simple, so the below example does not use any database for persistence. I will be writing a separate post involving a persistence layer and ORM.


Go installation

The only pre-requisite for getting this working is to have a working environment setup for Go. As long as you have installed Go, and validated your setup , we should be good to go. Also take a look at the code organization guidelines, so that you have a clear idea about the directory structure.


Imports

Now that we have our environment setup, it’s time to write some code and get our hands dirty. Let’s start by importing the set of packages mentioned in the code snippet below:

import (
    "encoding/json"
    "log"
    "net/http"
    "strconv"
    "time"

    "github.com/gorilla/mux"
)

encoding/json - This package contains methods that are used to convert Go types to JSON and vice-versa (This conversion is called as encode/decode in Go, serialization/de-serialization or marshall/unmarshall in other languages).

log - Has methods for formatting and printing log messages.

net/http - Contains methods for performing operations over HTTP. It provides HTTP server and client implementations and has abstractions for HTTP request, response, headers, etc.

strconv - Contains methods that convert string from/to other datatypes.

time - Provides methods for handling(storing/displaying/manipulating) time values.

github.com/gorilla/mux - Provides methods to route incoming http requests to their respective handler methods

Mux is a third-party library and we have to explicitly download and install it before using it in our application. Executing the following from the commandline will install the mux dependency in the $GOPATH/src directory:

go get -u github.com/gorilla/mux

Assuming you have set the GOPATH env variable correctly, You should see the github.com/gorilla/mux directory under $GOPATH/src.


Order model

The next step is to create a representation of an order for our service. For the purpose of this tutorial, let’s just have the basic set of fields needed for an order.

// Order represents the model for an order
type Order struct {
    OrderID      string    `json:"orderId"`
    CustomerName string    `json:"customerName"`
    OrderedAt    time.Time `json:"orderedAt"`
    Items        []Item    `json:"items"`
}

An order has the following fields declared inside the Order struct.

  • OrderId - id for each order

  • CustomerName - name of the customer

  • OrderedAt - the date/time at which it was placed

  • Items - list of items in the order.

A struct in go is a user-defined collection of fields. You can consider go structs as similar to classes in object -oriented programming languages, but with a few limitations (which I feel is outside the scope of this article). The statement type Order struct implies we are defining a new type called Order which is a struct(collection of fields) - The fields comprising the struct are specified inside the curly braces. As you can see above, each field consists of a name, a type and a tag(The string within the backticks at the end of each field declaration). Tags are used to specify metadata information about a specific field. For instance, the tag json:"orderId" means that , during decoding, the attribute named orderId in the json will be mapped to the OrderID field in the struct and vice-versa during encoding.

Similarly, each individual item in an order has the following fields declared in the Item struct.

// Item represents the model for an item in the order
type Item struct {
    ItemID      string `json:"itemID"`
    Description string `json:"description"`
    Quantity    int    `json:"quantity"`
}

Since we are not using a database to store data, we define the following variable to store our orders:

This definition means that orders is a slice that contains elements of type Order. Now what is a slice, and why are we using it here ? A slice is similar to an array, but it can be resized dynamically (unlike an array). In our example, we will be creating/deleting an arbitrary number of orders, and we do not know the number of orders beforehand. This is where a slice comes in handy - It re-sizes automatically under the hood, when we add/remove elements from it.

We also need to generate and assign the orderID for each order we create - For this, we maintain another variable named prevOrderId (initialized to 0), which stores the orderID of the order created previously. While creating a new order, we just need to increment this and assign to the OrderID field of the Order struct (as you will see below for the createOrder method).


Routes Definition

Let’s get started by defining the routes for our APIs inside the main function.

The first line creates a new mux Router. Before we proceed further, What exactly is a route? It is a way of specifying which function handles a certain API request.We can consider a route as a mapping between an API and the function that handles the API request. With the Gorilla Mux router, routes are defined using the HandleFunc method - The first argument is the API path, and the second argument is the name of the method that should be executed for that API. The Method function at the end specifies the HTTP method to be matched (GET, POST, PUT, etc). For example, with the above code snippet, POST API requests to the /orders URL is routed to the createOrder method (which we will define shortly).

Once the routes are defined, the Mux router directs incoming requests to their respective handler methods. Mux also supports more advanced use-cases like matching based on headers, query params, etc but we won’t need them for our example. Here, we register 5 routes (create, read, read-all, update and delete), mapping URL paths to their respective handler methods.

The ListenAndServe method starts an HTTP server listening at the 8080 port, and the wrapping log.Fatal ensures that errors are captured if the HTTP server fails.


1. Create API

Now, let’s code the API for creating an order.

The func keyword indicates we are defining a function named createOrder, which accepts 2 arguments - http.ResponseWriter, which contains the response details(headers, payload) - http.Request, which contains the incoming request details

json.NewDecoder(r.Body) converts the body of the incoming HTTP request and populates the appropriate fields in the order variable we have defined. The we set the OrderID field by incrementing the prevOrderID variable, and converting it to a String using the strconv function. Now that we have the order constructed, it needs to be added to our slice.

The append function does just that - adds the order to the orders slice defined earlier. The next line sets the Content-Type header to application/json which signifies that the function/API returns a JSON content as response.

Normally, the response of create API(POST) contains the representation of the resource that was created. In our case , the order variable has the picture of the order we just created. All we are doing in the last line of the code snippet is creating a new Encoder that encodes/converts the order variable to a JSON which is sent to the caller as the response.


2. Read and Read-all API

In this section, we are going to create APIs for reading/getting the orders we create through the createOrder API we discussed above. We are going to code 2 variations of this API - One to get all orders, and the other to get an order corresponding to a given orderId.

func getOrders(w http.ResponseWriter, r *http.Request) {
    w.Header().Set("Content-Type", "application/json")
    json.NewEncoder(w).Encode(orders)
}

The getOrders function is relatively simple - First, we set the Content-Type header similar to the createOrder function. Then, we encode the orders slice(which holds all the orders) to JSON, which is returned as a response.

func getOrder(w http.ResponseWriter, r *http.Request) {
    w.Header().Set("Content-Type", "application/json")
    params := mux.Vars(r)
    inputOrderID := params["orderId"]
    for _, order := range orders {
        if order.OrderID == inputOrderID {
            json.NewEncoder(w).Encode(order)
            return
        }
    }
}

The getOrder function is supposed to return the order details for a specific orderId which is provided as a path param to the API. So, how do we get the orderId passed in the request? The mux.Vars(r) function returns the path params as a map. In Line4, we retrieve the orderId from the params map, and assign it to the inputOrderID variable.

Then, we need to iterate through the orders slice and find the order whose ID matches the inputOrderID. This is what the for loop in the above code does. If you are finding the syntax strange, let’s delve a little deeper.

The range keyword in Go is used to iterate over elements in a variety of data structures (slice, map). When ranging over an slice, two values are returned for each iteration - The first is the index, and the second is the element at the index. Let’s say we are printing all the orders in the slice. Based on our understanding, the following loop works just fine:

for i, order := range orders {
  fmt.Println(order)
} 

On a closer look, we are only printing the element, and aren’t doing anything with the index i, so it can be ignored with the blank identifier _. The above loop can be written in a more refined/idiomatic way as:

for _, order := range orders {
  fmt.Println(order)
} 

Now that we have mastered the for loop, we could see that the same syntax is used in the getOrder function , and(instead of printing the element) we check if the orderID for the order is the same as the one passed in the request. If yes, we encode it as JSON and return.


3. Update API

Next, we are going to see how to update the details of an order. As before, we start by setting the header and fetching the orderId param.

func updateOrder(w http.ResponseWriter, r *http.Request) {
    w.Header().Set("Content-Type", "application/json")
    params := mux.Vars(r)
    inputOrderID := params["orderId"]
    for i, order := range orders {
        if order.OrderID == inputOrderID {
            orders = append(orders[:i], orders[i+1:]...)
            var updatedOrder Order
            json.NewDecoder(r.Body).Decode(&updatedOrder)
            orders = append(orders, updatedOrder)
            json.NewEncoder(w).Encode(updatedOrder)
            return
        }
    }
}

The way we are going to update an order is by deleting the existing order first, and appending the updated order passed in the request. Go does not provide any built-in functions to delete an element from a slice, it is accomplished through the append function instead. Yes, it is a bit counter-intuitive to use append to actually delete an element - Let’s try to get some clarity on this.

To remove an element from a slice, we slice out the elements before it, slice out the elements after it and append them both together. In our case, i is the index of the matching order - orders[:i] is the slice of elements occurring before i, and orders[i+1:] is the slice of elements occurring after i. These 2 slices are passed as arguments to the append function, and the result is a slice containing all elements from the original orders slice, except orders[i]. And don’t forget to add the ellipsis(three) dots after orders[i+1:] - It is needed to expand the argument(orders[i+1:]) to it’s individual elements. It is essential to use the ... operator with the second argument while trying to append 2 slices - Because from the definition of the append function, we can see that the 2nd argument is not a slice, but it’s a variadic function accepting an arbitary number of arguments of type T (of Type Order in our case).

Once the order is removed from the slice, the steps are similar to our createOrder function - We get the updated order from the request body, and build the updatedOrder variable. We add the updatedOrder to the orders slice with the append function (which we are an expert at, by now). Normally, the updated object is sent as a response for PUT API , and that’s exactly what we do, by encoding the updatedOrder as JSON.


4. Delete API

Finally, we have the delete API. Similar to the above APIs, we get the orderId path param from the request. Then we iterate through the orders slice and find the order with the input orderID. Once we find the matching order, we remove it from the slice using the append function, as we did in the updateOrder function. My preferred option for any DELETE API is to not return a response body and use the 204 No Content as the HTTP status code (Some DELETE implementations respond with a 200 OK and the deleted resource as the response body).

We then exit from the function using the return keyword.

func deleteOrder(w http.ResponseWriter, r *http.Request) {
    w.Header().Set("Content-Type", "application/json")
    params := mux.Vars(r)
    inputOrderID := params["orderId"]
    for i, order := range orders {
        if order.OrderID == inputOrderID {
            orders = append(orders[:i], orders[i+1:]...)
            w.WriteHeader(http.StatusNoContent)
            return
        }
    }
}

Running & Testing the App

Finally, we are done with all the APIs, and it’s time take them for a spin. To run the app, navigate to your project directory, and run the following commands:

go build go-orders-api
./go-orders-api

Create Order

curl -H 'Content-Type: application/json' -d '{"orderedAt":"2019-11-09T21:21:46+00:00","customerName":"Tom Jerry","items":[{"itemId":"123","description":"IPhone 10X","quantity":1}]}' -X POST http://localhost:8080/orders

Get Orders

curl http://localhost:8080/orders

Update Order

curl -H 'Content-Type: application/json' -d '{"orderId":"1","orderedAt":"2019-11-09T21:21:46+00:00","items":[{"itemId":"123","description":"IPhone 10X","quantity":3}]}' -X PUT http://localhost:8080/orders/1

Delete Order

curl -X DELETE http://localhost:8080/orders/1

You can also use tools like Postman to try these requests out.


Conclusion

If you have managed to get this API up and running, give yourself a pat on the back (I realize it has been a long post, though I tried my best to keep it short) 😄 We have learned how to use the built-in net/http library and the gorilla/mux library to build a REST API in Golang. You can checkout the complete code from Github (It has only one file anyway 😄). Please do comment if you see any questions/issues with the code - I’ll be glad to help out.
A logical next step would be to build an API that uses a database like MySQL to persist and retrieve the data. I’ll try to write a post on that shortly.


See Also