The final piece of the puzzle is the handlePolls
function that will use the helpers to understand the incoming request and access the database, and generate a meaningful response that will be sent back to the client. We also need to model the poll data that we were working with in the previous chapter.
Create a new file called polls.go
, and add the following code:
package main import "gopkg.in/mgo.v2/bson" type poll struct { ID bson.ObjectId `bson:"_id" json:"id"` Title string `json":"title""` Options []string `json:"options"` Results map[string]int `json:"results,omitempty"` }
Here we define a structure called poll
that has three fields that in turn describe the polls being created and maintained by the code we wrote in the previous chapter. Each field also has a tag (two in the ID
case), which allows us to provide some extra metadata.
Tags are strings that follow a field definition within a struct
type on the same line of code. We use the back tick character to denote literal strings, which means we are free to use double quotes within the tag string itself. The reflect
package allows us to pull out the value associated with any key; in our case, both bson
and json
are examples of keys, and they are each key/value-pair-separated by a space character. Both the encoding/json
and gopkg.in/mgo.v2/bson
packages allow you to use tags to specify the field name that will be used with encoding and decoding (along with some other properties), rather than having it infer the values from the name of the fields themselves. We are using BSON to talk with the MongoDB database and JSON to talk to the client, so we can actually specify different views of the same struct
type. For example, consider the ID field:
ID bson.ObjectId `bson:"_id" json:"id"`
The name of the field in Go is ID
, the JSON field is id
, and the BSON field is _id
, which is the special identifier field used in MongoDB.
Because our simple path-parsing solution cares only about the path, we have to do some extra work when looking at the kind of RESTful operation the client is making. Specifically, we need to consider the HTTP method so we know how to handle the request. For example, a GET
call to our /polls/
path should read polls, where a POST
call would create a new one. Some frameworks solve this problem for you, by allowing you to map handlers based on more than the path, such as the HTTP method or the presence of specific headers in the request. Since our case is ultra simple, we are going to use a simple switch
case. In polls.go
, add the handlePolls
function:
func handlePolls(w http.ResponseWriter, r *http.Request) { switch r.Method { case "GET": handlePollsGet(w, r) return case "POST": handlePollsPost(w, r) return case "DELETE": handlePollsDelete(w, r) return } // not found respondHTTPErr(w, r, http.StatusNotFound) }
We switch on the HTTP method and branch our code depending on whether it is GET
, POST
, or DELETE
. If the HTTP method is something else, we just respond with a 404 http.StatusNotFound
error. To make this code compile, you can add the following function stubs underneath the handlePolls
handler:
func handlePollsGet(w http.ResponseWriter, r *http.Request) { respondErr(w, r, http.StatusInternalServerError, errors.New("not implemented")) } func handlePollsPost(w http.ResponseWriter, r *http.Request) { respondErr(w, r, http.StatusInternalServerError, errors.New("not implemented")) } func handlePollsDelete(w http.ResponseWriter, r *http.Request) { respondErr(w, r, http.StatusInternalServerError, errors.New("not implemented")) }
In this section, we learned how to manually parse elements of the requests (the HTTP method) and make decisions in code. This is great for simple cases, but it's worth looking at packages such as Goweb or Gorilla's mux
package for some more powerful ways of solving these problems. Nevertheless, keeping external dependencies to a minimum is a core philosophy of writing good and contained Go code.
Now it's time to implement the functionality of our web service. Inside the GET
case, add the following code:
func handlePollsGet(w http.ResponseWriter, r *http.Request) { db := GetVar(r, "db").(*mgo.Database) c := db.C("polls") var q *mgo.Query p := NewPath(r.URL.Path) if p.HasID() { // get specific poll q = c.FindId(bson.ObjectIdHex(p.ID)) } else { // get all polls q = c.Find(nil) } var result []*poll if err := q.All(&result); err != nil { respondErr(w, r, http.StatusInternalServerError, err) return } respond(w, r, http.StatusOK, &result) }
The very first thing we do in each of our subhandler functions is to use GetVar
to get the mgo.Database
object that will allow us to interact with MongoDB. Since this handler was nested inside both withVars
and withData
, we know that the database will be available by the time execution reaches our handler. We then use mgo
to create an object referring to the polls
collection in the database—if you remember, this is where our polls live.
We then build up an mgo.Query
object by parsing the path. If an ID is present, we use the FindId
method on the polls
collection, otherwise we pass nil
to the Find
method, which indicates that we want to select all the polls. We are converting the ID from a string to a bson.ObjectId
type with the ObjectIdHex
method so that we can refer to the polls with their numerical (hex) identifiers.
Since the All
method expects to generate a collection of poll objects, we define the result as []*poll
, or a slice of pointers to poll types. Calling the All
method on the query will cause mgo
to use its connection to MongoDB to read all the polls and populate the result
object.
Now that we have added some functionality, let's try out our API for the first time. If you are using the same MongoDB instance that we set up in the previous chapter, you should already have some data in the polls
collection; to see our API working properly, you should ensure there are at least two polls in the database.
If you need to add other polls to the database, in a terminal, run the mongo
command to open a database shell that will allow you to interact with MongoDB. Then enter the following commands to add some test polls:
> use ballots switched to db ballots > db.polls.insert({"title":"Test poll","options":["one","two","three"]}) > db.polls.insert({"title":"Test poll two","options":["four","five","six"]})
In a terminal, navigate to your api
folder, and build and run the project:
go build –o api ./api
Now make a GET
request to the /polls/
endpoint by navigating in your browser to http://localhost:8080/polls/?key=abc123
; remember to include the trailing slash. The result will be an array of polls in JSON format.
Copy and paste one of the IDs from the polls list, and insert it before the ?
character in the browser to access the data for a specific poll; for example, http://localhost:8080/polls/5415b060a02cd4adb487c3ae?key=abc123
. Notice that instead of returning all the polls, it only returns one.
You might have also noticed that although we are only returning a single poll, this poll value is still nested inside an array. This is a deliberate design decision made for two reasons: the first and most important reason is that nesting makes it easier for users of the API to write code to consume the data. If users are always expecting a JSON array, they can write strong types that describe that expectation, rather than having one type for single polls and another for collections of polls. As an API designer, this is your decision to make. The second reason we left the object nested in an array is that it makes the API code simpler, allowing us to just change the mgo.Query
object and to leave the rest of the code the same.
Clients should be able to make a POST
request to /polls/
to create a poll. Let's add the following code inside the POST
case:
func handlePollsPost(w http.ResponseWriter, r *http.Request) { db := GetVar(r, "db").(*mgo.Database) c := db.C("polls") var p poll if err := decodeBody(r, &p); err != nil { respondErr(w, r, http.StatusBadRequest, "failed to read poll from request", err) return } p.ID = bson.NewObjectId() if err := c.Insert(p); err != nil { respondErr(w, r, http.StatusInternalServerError, "failed to insert poll", err) return } w.Header().Set("Location", "polls/"+p.ID.Hex()) respond(w, r, http.StatusCreated, nil) }
Here we first attempt to decode the body of the request that, according to RESTful principles, should contain a representation of the poll object the client wants to create. If an error occurs, we use the respondErr
helper to write the error to the user, and immediately return the function. We then generate a new unique ID for the poll, and use the mgo
package's Insert
method to send it into the database. As per HTTP standards, we then set the Location
header of the response and respond with a 201 http.StatusCreated
message, pointing to the URL from which the newly created poll maybe accessed.
The final piece of functionality we are going to include in our API is the capability to delete polls. By making a request with the DELETE
HTTP method to the URL of a poll (such as /polls/5415b060a02cd4adb487c3ae
), we want to be able to remove the poll from the database and return a 200 Success
response:
func handlePollsDelete(w http.ResponseWriter, r *http.Request) { db := GetVar(r, "db").(*mgo.Database) c := db.C("polls") p := NewPath(r.URL.Path) if !p.HasID() { respondErr(w, r, http.StatusMethodNotAllowed, "Cannot delete all polls.") return } if err := c.RemoveId(bson.ObjectIdHex(p.ID)); err != nil { respondErr(w, r, http.StatusInternalServerError, "failed to delete poll", err) return } respond(w, r, http.StatusOK, nil) // ok }
Similar to the GET
case, we parse the path, but this time we respond with an error if the path does not contain an ID. For now, we don't want people to be able to delete all polls with one request, and so use the suitable StatusMethodNotAllowed
code. Then, using the same collection we used in the previous cases, we call RemoveId
, passing in the ID in the path after converting it into a bson.ObjectId
type. Assuming things go well, we respond with an http.StatusOK
message, with no body.
In order for our DELETE
capability to work over CORS, we must do a little extra work to support the way CORS browsers handle some HTTP methods such as DELETE
. A CORS browser will actually send a pre-flight request (with an HTTP method of OPTIONS
) asking for permission to make a DELETE
request (listed in the Access-Control-Request-Method
request header), and the API must respond appropriately in order for the request to work. Add another case in the switch
statement for OPTIONS
:
case "OPTIONS": w.Header().Add("Access-Control-Allow-Methods", "DELETE") respond(w, r, http.StatusOK, nil) return
If the browser asks for permission to send a DELETE
request, the API will respond by setting the Access-Control-Allow-Methods
header to DELETE
, thus overriding the default *
value that we set in our withCORS
wrapper handler. In the real world, the value for the Access-Control-Allow-Methods
header will change in response to the request made, but since DELETE
is the only case we are supporting, we can hardcode it for now.
The details of CORS are out of the scope of this book, but it is recommended that you research the particulars online if you intend to build truly accessible web services and APIs. Head over to http://enable-cors.org/ to get started.
curl
is a command-line tool that allows us to make HTTP requests to our service so that we can access it as though we were a real app or client consuming the service.
Windows users do not have access to curl
by default, and will need to seek an alternative. Check out http://curl.haxx.se/dlwiz/?type=bin or search the Web for "Windows curl
alternative".
In a terminal, let's read all the polls in the database through our API. Navigate to your api
folder and build and run the project, and also ensure MongoDB is running:
go build –o api ./api
We then perform the following steps:
curl
command that uses the -X
flag to denote we want to make a GET
request to the specified URL:curl -X GET http://localhost:8080/polls/?key=abc123
[{"id":"541727b08ea48e5e5d5bb189","title":"Best Beatle?","options":["john","paul","george","ringo"]},{"id":"541728728ea48e5e5d5bb18a","title":"Favorite language?","options":["go","java","javascript","ruby"]}]
curl --data '{"title":"test","options":["one","two","three"]}' -X POST http://localhost:8080/polls/?key=abc123
curl -X GET http://localhost:8080/polls/?key=abc123
curl -X GET http://localhost:8080/polls/541727b08ea48e5e5d5bb189?key=abc123 [{"id":"541727b08ea48e5e5d5bb189",","title":"Best Beatle?","options":["john","paul","george","ringo"]}]
Best Beatle
. Let's make a DELETE
request to remove the poll:curl -X DELETE http://localhost:8080/polls/541727b08ea48e5e5d5bb189?key=abc123
Best Beatle
poll has gone:curl -X GET http://localhost:8080/polls/?key=abc123 [{"id":"541728728ea48e5e5d5bb18a","title":"Favorite language?","options":["go","java","javascript","ruby"]}]
So now that we know that our API is working as expected, it's time to build something that consumes the API properly.