Handling endpoints

The final piece of the puzzle is the handlePolls function, which 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"` 
  APIKey  string         `json:"apikey"` 
} 

Here, we define a structure called poll, which has five fields that in turn describe the polls being created and maintained by the code we wrote in the previous chapter. We have also added the APIKey field, which you probably wouldn't do in the real world but which will allow us to demonstrate how we extract the API key from the context. 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 just a string that follows a field definition within a struct type on the same line. We use the black 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 that 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 (s *Server) handlePolls(w http.ResponseWriter,
 r *http.Request) { 
  switch r.Method { 
    case "GET": 
    s.handlePollsGet(w, r) 
    return 
    case "POST": 
    s.handlePollsPost(w, r) 
    return 
    case "DELETE": 
    s.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 GETPOST, 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 (s *Server) handlePollsGet(w http.ResponseWriter,
 r *http.Request) { 
  respondErr(w, r, http.StatusInternalServerError,
   errors.New("not    
  implemented")) 
} 
func (s *Server) handlePollsPost(w http.ResponseWriter,
 r *http.Request) { 
  respondErr(w, r, http.StatusInternalServerError,
   errors.New("not   
   implemented")) 
} 
func (s *Server) 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 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. Add the following code:

func (s *Server) handlePollsGet(w http.ResponseWriter,
 r *http.Request) { 
  session := s.db.Copy() 
  defer session.Close() 
  c := session.DB("ballots").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 sub handler functions is create a copy of the database session that will allow us to interact with MongoDB. 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, such as a small number of polls, this approach is fine, but as the polls grow, we will need to consider a more sophisticated approach. We can page the results by iterating over them using the Iter method on the query and using the Limit and Skip methods, so we do not try to load too much data into the memory or present too much information to users in one go.

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 to http://localhost:8080/polls/?key=abc123 in your browser; remember to include the trailing slash. The result will be an array of polls in the 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. Note 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 leave the rest of the code the same.

Creating a poll

Clients should be able to make a POST request to /polls/ in order to create a poll. Let's add the following code inside the POST case:

func (s *Server) handlePollsPost(w http.ResponseWriter, 
 r *http.Request) { 
  session := s.db.Copy() 
  defer session.Close() 
  c := session.DB("ballots").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 
  } 
  apikey, ok := APIKey(r.Context()) 
  if ok { 
    p.APIKey = apikey 
  } 
  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) 
} 

After we get a copy of the database session like earlier, we 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 exit from 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. 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 may be accessed. Some APIs return the object instead of providing a link to it; there is no concrete standard so it's up to you as the designer.

Deleting a poll

The final piece of functionality we are going to include in our API is the ability 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 (s *Server) handlePollsDelete(w http.ResponseWriter, 
  r *http.Request) { 
  session := s.db.Copy() 
  defer session.Close() 
  c := session.DB("ballots").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 we use the suitable StatusMethodNotAllowed code. Then, using the same collection we used in the previous cases, we call RemoveId, passing 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 preflight 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 hitEnter:
    [{"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. 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 Beatles 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
3.136.17.12