Keiran.SCOT

Musings of a Scottish Geek

Building a ToDo API with Golang and Kubernetes! – Part 2 – CRUD

Hi and welcome to Part 2 of Building a ToDo API with Golang and Kubernetes! In part 1 we learned what we were going to build and how to use mux to route an HTTP Request to a Handler function. In this part we will learn how to build our endpoints for CRUD(Create, Read, Update, Delete) operations, And how to use MongoDB to store our ToDo Items.

Key learning Points

  • Handling HTTP Methods with mux
  • How to use variables in your route path with mux
  • Using mgo to Manipulate data in MongoDB

MongoDB

MongoDB is a free and open-source cross-platform document-oriented database program. Classified as a NoSQL database program, MongoDB uses JSON-like documents with schemas.

Lets get started!

Running MongoDB

There are a number of ways you can run MongoDB, Today we will focus on running it in Docker as its simple, allows for easier cleanup and fits in with our chosen deployment mechanism of Kubernetes.

to run MongoDB on docker you can use the container provided on dockerhub

Its easy to run with a simple command.

$ docker run --rm -it -v mongo:/data/db -p 27017:27017 mongo

The switches I have used here are :

[table id=1 /]

And thats it MongoDB is now running! If you are unfamiliar with MongoDB I recommend you take one of their tutorials.

Setting up mgo in our project

Much like we done previously when we added mux we need to use the ‘go get’ command to get the mgo package.

$ go get gopkg.in/mgo.v2

Now we need to add the imports so add them under the import for mux.

import (
    "log"
    "net/http"
    "github.com/gorilla/mux"
    "gopkg.in/mgo.v2"
    "gopkg.in/mgo.v2/bson"
    "io"
    "time"
)

Mgo is now available to our project.

Later on in the tutorial we will be using the time and encoding/json libraries from go. Please add it to your imports.

Setting up a connection to MongoDB

Now we have mgo imported we can open a connection to our MongoDB instance. As the port was exposed when we launched Mongo using Docker we can connect to 127.0.0.1 / ::1 / localhost. We connect using the mgo.Dial method, We also need to setup a client to connect to our MongoDB collection, Don’t worry about creating the Database and Collection, Mongo Handles this for us. We need this available globally so add it under your imports.

var session, _ = mgo.Dial("127.0.0.1")
var c = session.DB("TutDb").C("ToDo")

You may have noticed that I have used 2 variables (session and _) when calling mgo.Dial. The Dial method has 2 seperate return values, Session and Error. To handle the multiple return values we need to declare multiple variables together. If there is no error the _ variable is nil, if there is an error session will be nil.

We are almost there, Now we need to add a couple of things to our main() function.

Monatonic Mode

In the Monotonic consistency mode reads may not be entirely up-to-date, but they will always see the history of changes moving forward, the data read will be consistent across sequential queries in the same session, and modifications made within the session will be observed in following queries (read-your-writes).

In practice, the Monotonic mode is obtained by performing initial reads on a unique connection to an arbitrary secondary, if one is available, and once the first write happens, the session connection is switched over to the primary server. This manages to distribute some of the reading load with secondaries, while maintaining some useful guarantees.

We set the mode in mgo using the SetMode method. Add this to your main method.

func main() {
     session.SetMode(mgo.Monotonic, true)
     defer session.Close()
     router := mux.NewRouter()
     router.HandleFunc("/health", Health).Methods("GET")
     log.Fatal(http.ListenAndServe(":8000", router))
 }

You may have noticed I also added a defer session.Close() in there. The defer keyword is used to execute a method after the calling method had returned. This allows us to cleanup our connection once our main method has returned.

The ToDoItem Struct

Before we can build our CRUD methods we need to know how the data will be stored in the database. We can do this using the go struct. We use a struct as the mgo library returns and requires an interface. By providing the struct our data is automatically mapped to entries in our MongoDB Collection so we shouldn’t need to do much manual querying.

type ToDoItem struct {
    ID      bson.ObjectId `bson:"_id,omitempty"`
    Date time.Time
    Description string
    Done bool
}

The type keyword introduces a new type. It’s followed by the name of the type (ToDoItem), the keyword struct to indicate that we are defining a struct type and a list of fields inside of curly braces. Each field has a name and a type.

Lets put the C in CRUD

So the first method we are going to handle will allow us to add items to our ToDo list. We will handle 2 HTTP Verbs here. POST and PUT. We will pass our description to the endpoint as a form value. This makes it significantly easier to write a front end to our API in the future.

Lets add a Handler to our main function.

func main() {
    session.SetMode(mgo.Monotonic, true)
    defer session.Close()
    router := mux.NewRouter()
    router.HandleFunc("/todo", AddToDo).Methods("POST", "PUT")
    router.HandleFunc("/health", Health).Methods("GET")
    log.Fatal(http.ListenAndServe(":8000", router))
}

So now we have a route any POST or PUT messages to /todo will be directed to the AddToDo method.

The AddToDo method

Since we have already added the Handler to the router we should go ahead and write the AddToDo method.

func AddToDo(w http.ResponseWriter, r *http.Request) {
    _ = c.Insert(ToDoItem{
        bson.NewObjectId(),
        time.Now(),
        r.FormValue("description"),
        false,
    })

    result := ToDoItem{}
    _ = c.Find(bson.M{"description": r.FormValue("description")}).One(&result)
    json.NewEncoder(w).Encode(result)
}

In our handler function we always need to pass in http.ResponseWriter and http.Request in order to process the HTTP Requests and return a value.

we make use of our global variable _ to store any output from the mgo.Collection.Insert we defined as c.

We are Inserting a new item of type ToDoItem.

Using bson we generate an ObjectId for our entry.

we use the r.FormValue to obtain the value for description.

We then lookup our Entry and return it to the user as JSON.

Lets test our Add method!

I will be testing our new API Method using cURL, however you can use whatever you wish to test it. Tools like Postman are good for a beginner.

So lets run the application using go run

$ go run main.go

There will be no output at this stage. Now in a seperate terminal (or tool of your choice) lets make an call to POST and PUT to our endpoint.

$ curl -X POST -d "description=POST Todo Item" 127.0.0.1:8000/todo
{"ID":"5a9c0983616ca1dc159bb385","Date":"2018-03-04T14:58:11.568Z","Description":"POST Todo Item","Done":false}
$ curl -X PUT -d "description=PUT Todo Item" 127.0.0.1:8000/todo
{"ID":"5a9c0979616ca1dc159bb384","Date":"2018-03-04T14:58:01.61Z","Description":"PUT Todo Item","Done":false}

If all went well you should have the output from above!

Reading our Data

Now that we have some data we can now work on getting the data out to the user via a GET Request to the /todo endpoint so lets define that endpoint in our router in the main method.

...
     router.HandleFunc("/todo", GetToDo).Methods("GET")
     route.HandleFunc("/todo/{id}", GetToDo).Methods("GET") // Define a route with an id Variable
...

Now we have a handler for out method we need to define the method. Before we do that we need to write a method to GetByID

func GetByID(id string) []ToDoItem {
     var result ToDoItem
     var res []ToDoItem
     _ = c.Find(bson.M{"_id": bson.ObjectIdHex(id)}).One(&result)
     res = append(res, result)
     return res
 }

The above method will be used when we pass a value to the /todo/{id} route. This method is will return an Array of ToDoItem from our Database using the Find method.

Now we can write our GetToDo method

func GetToDo(w http.ResponseWriter, r *http.Request) {
    var res []ToDoItem

    vars := mux.Vars(r)
    id := vars["id"]
    if id != "" {
        res = GetByID(id)
    } else {
        _ = c.Find(nil).All(&res)
    }

    json.NewEncoder(w).Encode(res)
}

The big difference in this method is the use of mux.Vars this parses the variables we define in our routes. mux variables are always considered strings.

Another difference is the Find method used if no ID is set, using the mgo library if we hass nil as a Param to the Find method it will return all rows.

When a user hits this endpoint they will be provided with a JSON result of the result. Lets try it out. Start up main.go again

$ curl 127.0.0.1:8000/todo
[{"ID":"5a9c0979616ca1dc159bb384","Date":"2018-03-04T14:58:01.61Z","Description":"PUT Todo Item","Done":false},{"ID":"5a9c0983616ca1dc159bb385","Date":"2018-03-04T14:58:11.568Z","Description":"POST Todo Item","Done":false}]

Now we can retreive all of the items, lets see if we can retreive a single item.

$ curl 127.0.0.1:8000/todo/5a9c0979616ca1dc159bb384
[{"ID":"5a9c0979616ca1dc159bb384","Date":"2018-03-04T14So :58:01.61Z","Description":"PUT Todo Item","Done":false}]

awesome so now we can read our data.

Updating a field

What good is a ToDo list if you can’t mark things done. Our next CRUD operation will deal with that, We will handle a PATCH request to mark an item done. There will be no parameters to the request but we will use another mux variable here.

Lets define our route.

...
    router.HandleFunc("/todo/{id}", MarkDone).Methods("PATCH")
...

Now we need to define the MarkDone Method

func MarkDone(w http.ResponseWriter, r*http.Request) {
    vars := mux.Vars(r)
    id := bson.ObjectIdHex(vars["id"])
    err := c.Update(bson.M{"_id": id}, bson.M{"$set": bson.M{"done": true}})
    if err != nil {
        w.WriteHeader(http.StatusNotFound)
        w.Header().Set("Content-Type", "application/json")
        io.WriteString(w, `{"updated": false, "error": ` + err.Error() + `}` )
    } else {
        w.WriteHeader(http.StatusOK)
        w.Header().Set("Content-Type", "application/json")
        io.WriteString(w, `{"updated": true}`)
    }
}

Again we are using mux variables to get the id from the URI String.

We then need to encode it as an ObjectIdHex calue for Mongo to understand that its an ID string.

We need to handle errors here so that we don’t panic and crash the application.

We then call Update on the collection to update the record, Using a bson.M (Bson Map) we map the id to the _id field set in Mongo, Then call a set operation to set update the done field.

If Update is successful err will not be set as Update returns nil on success, If it returns something though it means the operation was not successful.

Lets test it

$ curl -X PATCH 127.0.0.1:8000/todo/5a9c0979616ca1dc159bb384
{"updated": true}

Awesome it works. Lets try getting it again

$ curl 127.0.0.1:8000/todo/5a9c0979616ca1dc159bb384
[{"ID":"5a9c0979616ca1dc159bb384","Date":"2018-03-04T14:58:01.61Z","Description":"PUT Todo Item","Done":true}]

As the patch was sucessful the done field is now marked as true.

But what if a record doesnt exist

$ curl -X PATCH 127.0.0.1:8000/todo/5a9c0979616ca1dc159bb312
{"updated": false, "error": not found}

So or error handler works too, Thats awesome.

Deleting an Item

So what if our user made a mistake or the Item is no longer valid, We need to provide a way to delete it. We want to be able to do this by sending a DELETE request to the item. So lets define the route

...
    router.HandleFunc("/todo/{id}", DelToDo).Methods("DELETE")
...

Again we are using a mux variable, In a RESTful webservice the ability to work with URI strings makes life easier for both the End User and Developers, It sure as hell beats using a request string.

Now lets build the delete method.

func DelToDo(w http.ResponseWriter, r*http.Request) {
    vars := mux.Vars(r)
    id := vars["id"]
    err := c.RemoveId(bson.ObjectIdHex(id))
    if err == mgo.ErrNotFound  {
        json.NewEncoder(w).Encode(err.Error())
    } else {
        io.WriteString(w, "{result: 'OK'}")
    }
}

Not much new here except the RemoveId method. This method will remove the record with the matching ID passed from the URI variable. We also make a little use of the io.WriteString we used in our health check in Part 1.

The big question is, Does it work.

$ curl -X DELETE 127.0.0.1:8000/todo/5a9c0983616ca1dc159bb385
{result: 'OK'}

Now if we run our GET request again

$ curl 127.0.0.1:8000/todo 
[{"ID":"5a9c0979616ca1dc159bb384","Date":"2018-03-04T14:58:01.61Z","Description":"PUT Todo Item","Done":false}]

We have successfully deleted our item!

Conclusions

Congratulations you have just built your first (insecure) RESTful web service using Go. Come back soon for part 3 where you will add Authentication Middleware to your application to make secure before we deploy it.

The code For Part 2 is available on Github