Handling endpoints

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.

Using tags to add metadata to structs

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.

Many operations with a single handler

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"))
}

Tip

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.

Reading polls

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.

Note

For small scale projects, such as a small number of polls, this approach is fine, but as the number of polls grow, we would need to consider paging the results or even iterating over them using the Iter method on the query, so we do not try to load too much data into memory.

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.

Tip

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.

Tip

Test the API key functionality by removing or changing the key parameter to see what the error looks like.

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.

Creating a poll

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.

Deleting a poll

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.

CORS support

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.

Note

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.

Testing our API using curl

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.

Note

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:

  1. Enter the following 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
    
  2. The output is printed after you hit Enter:
    [{"id":"541727b08ea48e5e5d5bb189","title":"Best Beatle?","options":["john","paul","george","ringo"]},{"id":"541728728ea48e5e5d5bb18a","title":"Favorite language?","options":["go","java","javascript","ruby"]}]
    
  3. While it isn't pretty, you can see that the API returns the polls from your database. Issue the following command to create a new poll:
    curl --data '{"title":"test","options":["one","two","three"]}' -X POST http://localhost:8080/polls/?key=abc123
    
  4. Get the list again to see the new poll included:
    curl -X GET http://localhost:8080/polls/?key=abc123
    
  5. Copy and paste one of the IDs, and adjust the URL to refer specifically to that poll:
    curl -X GET http://localhost:8080/polls/541727b08ea48e5e5d5bb189?key=abc123
    [{"id":"541727b08ea48e5e5d5bb189",","title":"Best Beatle?","options":["john","paul","george","ringo"]}]
    
  6. Now we see only the selected poll, Best Beatle. Let's make a DELETE request to remove the poll:
    curl -X DELETE http://localhost:8080/polls/541727b08ea48e5e5d5bb189?key=abc123
    
  7. Now when we get all the polls again, we'll see that the 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.

..................Content has been hidden....................

You can't read the all page of ebook, please click here login for view all page.
Reset
18.226.104.153