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 orderCustomerName
- name of the customerOrderedAt
- the date/time at which it was placedItems
- 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:
var orders []Order
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.
var prevOrderID = 0
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.
func main() {
router := mux.NewRouter()
// Create
router.HandleFunc("/orders", createOrder).Methods("POST")
// Read
router.HandleFunc("/orders/{orderId}", getOrder).Methods("GET")
// Read-all
router.HandleFunc("/orders", getOrders).Methods("GET")
// Update
router.HandleFunc("/orders/{orderId}", updateOrder).Methods("PUT")
// Delete
router.HandleFunc("/orders/{orderId}", deleteOrder).Methods("DELETE")
// Swagger
router.PathPrefix("/swagger").Handler(httpSwagger.WrapHandler)
log.Fatal(http.ListenAndServe(":8080", router))
}
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.
func createOrder(w http.ResponseWriter, r *http.Request) {
var order Order
json.NewDecoder(r.Body).Decode(&order)
prevOrderID++
order.OrderID = strconv.Itoa(prevOrderID)
orders = append(orders, order)
w.Header().Set("Content-Type", "application/json")
json.NewEncoder(w).Encode(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
- Build a REST API in Golang with MySQL, GORM and Gorilla Mux
- Consuming REST APIs in Go - HTTP GET, PUT, POST and DELETE
- Swagger UI setup for Go REST API using Swaggo