Building a REST Service with Golang - Part 3 (Adding a Backend)
Intro
Golang is pretty hot right now. Over the past few weeks, I've been exploring implementing some of the cloud infrastracture I'd previously built with node in go, partly for fun, partly because go is fast. This led me to believe that setting up a basic REST service would make for a great tutorial.
At the end, we will have a fully functioning REST service for serving basic CRUD on a user resource. Code for the final product can be found here.
This is the third and final entry in a series of high level tutorials on setting up a REST service using golang, focused on wiring up our previous webserver to a mongo backend.
Setup mgo
At the end of the last tutorial, we had a functioning, albeit useless, webserver offering some CRUD operations on a user
resource. In this entry, we will be tying that to a mongodb backend for persistent data. If you don't have mongo installed on your system, you can find instructions here or install via brew
.
The most popular mongo driver for golang is easily mgo. We'll be using two packages provided by mgo
, mgo
itself and bson
. We need to grab each package to get going.
$ go get gopkg.in/mgo.v2
$ go get gopkg.in/mgo.v2/bson
We also need to establish a connection to mongo to provide to our controllers. Add the following function to our server.go
.
func getSession() *mgo.Session {
// Connect to our local mongo
s, err := mgo.Dial("mongodb://localhost")
// Check if connection error, is mongo running?
if err != nil {
panic(err)
}
return s
}
Now, our controller is going to need a mongo session to use in the CRUD methods. Let's change how we get a UserController
to the following.
// Get a UserController instance
uc := controllers.NewUserController(getSession())
Update the UserController struct
The first thing we need to do to our UserController
is to extend the struct to contain a reference to a *mgo.Session
so that our controller methods can access mongo. Update the UserController
definition to match the following.
UserController struct {
session *mgo.Session
}
Next, we need to update our NewUserController
function to receive a *mgo.Session
and instantiate the controller with it. Update NewUserController
to match the following.
func NewUserController(s *mgo.Session) *UserController {
return &UserController{s}
}
Update the User Model
Before we start updating the controller methods to use mongo, we need to update our model to integrate with mgo
. Similar to how we updated the user model to use a json
struct tag for outputting JSON data, we need to add a struct tag to tell mgo
how to store the user information.
Update user.go
in the models
directory to contain the following.
package models
import "gopkg.in/mgo.v2/bson"
type (
// User represents the structure of our resource
User struct {
Id bson.ObjectId `json:"id" bson:"_id"`
Name string `json:"name" bson:"name"`
Gender string `json:"gender" bson:"gender"`
Age int `json:"age" bson:"age"`
}
)
You'll notice that we are importing only the bson
package from mgo
. The bson
package provides an implementation of the bson specification for go. We use this to change the type of our Id
field to a bson.ObjectId
.
We also extend each field with a bson struct tag to describe the property name of the mongo document containing the user.
Update the POST Controller
Now that our model has been updated and our controller has a reference to an active mongo session, let's start integrating mongo into our service by updating the CreateUser
method of our UserController
. Update the method to the following.
// CreateUser creates a new user resource
func (uc UserController) CreateUser(w http.ResponseWriter, r *http.Request, p httprouter.Params) {
// Stub an user to be populated from the body
u := models.User{}
// Populate the user data
json.NewDecoder(r.Body).Decode(&u)
// Add an Id
u.Id = bson.NewObjectId()
// Write the user to mongo
uc.session.DB("go_rest_tutorial").C("users").Insert(u)
// Marshal provided interface into JSON structure
uj, _ := json.Marshal(u)
// Write content-type, statuscode, payload
w.Header().Set("Content-Type", "application/json")
w.WriteHeader(201)
fmt.Fprintf(w, "%s", uj)
}
There are two significant changes here. First, we ask the bson
package for a new ObjectId to store on our user. Second, we write the user to the users
collection in the go_rest_tutorial
database.
Let's test adding a user.
$ curl -XPOST -H 'Content-Type: application/json' -d '{"name": "Bob Smith", "gender": "male", "age": 50}' http://localhost:3000/user
{"id":"5497246c380a967ff1000003","name":"Bob Smith","gender":"male","age":50}
Awesome! Our user was stored in mongo and we have successfully generated an ObjectId.
Update the GET Controller
Now that we are able to create new users, it would be nice to be able to fetch them as well. To do this, let's update the GetUser
method of our UserController
to the following.
// GetUser retrieves an individual user resource
func (uc UserController) GetUser(w http.ResponseWriter, r *http.Request, p httprouter.Params) {
// Grab id
id := p.ByName("id")
// Verify id is ObjectId, otherwise bail
if !bson.IsObjectIdHex(id) {
w.WriteHeader(404)
return
}
// Grab id
oid := bson.ObjectIdHex(id)
// Stub user
u := models.User{}
// Fetch user
if err := uc.session.DB("go_rest_tutorial").C("users").FindId(oid).One(&u); err != nil {
w.WriteHeader(404)
return
}
// Marshal provided interface into JSON structure
uj, _ := json.Marshal(u)
// Write content-type, statuscode, payload
w.Header().Set("Content-Type", "application/json")
w.WriteHeader(200)
fmt.Fprintf(w, "%s", uj)
}
Again, there a couple of significant changes. First, we are converting our id
path parameter to a bson.ObjectId
to be used to find the user. In the event that the id cannot be converted, we bail with a 404 Not Found. Second, we use our mongo session to find a user matching the provided id from the users
collection. We also added an error check when we find the user to deliver a 404 Not Found.
Let's test it out by grabbing the user we created previously.
$ curl http://localhost:3000/user/5497246c380a967ff1000003
{"id":"5497246c380a967ff1000003","name":"Bob Smith","gender":"male","age":50}
Sweet! We got the same user that we had posted above. Now if we change the id we should get a 404.
$ curl -i http://localhost:3000/user/5497246c380a967ff1000004
HTTP/1.1 404 Not Found
Date: Sun, 21 Dec 2014 19:58:34 GMT
Content-Length: 0
Content-Type: text/plain; charset=utf-8
Update the DELETE Controller
So we are able to create new users and retrieve them, now let's add the ability to remove them. To accomplish this, let's update the RemoveUser
method of our controller to the following.
// RemoveUser removes an existing user resource
func (uc UserController) RemoveUser(w http.ResponseWriter, r *http.Request, p httprouter.Params) {
// Grab id
id := p.ByName("id")
// Verify id is ObjectId, otherwise bail
if !bson.IsObjectIdHex(id) {
w.WriteHeader(404)
return
}
// Grab id
oid := bson.ObjectIdHex(id)
// Remove user
if err := uc.session.DB("go_rest_tutorial").C("users").RemoveId(oid); err != nil {
w.WriteHeader(404)
return
}
// Write status
w.WriteHeader(200)
}
As you can see, we are again getting an ObjectId out of the id
path parameter, and bailing with a 404 if the id cannot by converted to an ObjectId. We then remove the document matching this id and deliver a 200.
We can test this by first deleting the user we created, then trying to fetch the user.
$ curl -XDELETE http://localhost:3000/user/5497246c380a967ff1000003
$ curl -i http://localhost:3000/user/5497246c380a967ff1000003
HTTP/1.1 404 Not Found
Date: Sun, 21 Dec 2014 20:04:28 GMT
Content-Length: 0
Content-Type: text/plain; charset=utf-8
Great! We removed the user and confirmed that the user no longer exists.
Fin
This is end of the tutorial on adding a backend to our REST service, and also the end of the tutorial series in general. While there most certainly are many improvements to be made, such as adding some security and much better error handling, hopefully this high-level overview has been beneficial.