Exposing data operations over HTTP

Now that we have built all of our entities and the data access methods that operate on them, it's time to wire them up to an HTTP API. This will feel more familiar as we have already done this kind of thing a few times in the book.

Optional features with type assertions

When you use interface types in Go, you can perform type assertions to see whether the objects implement other interfaces, and since you can write interfaces inline, it is possible to very easily find out whether an object implements a specific function.

If v is interface{}, we can see whether it has the OK method using this pattern:

if obj, ok := v.(interface{ OK() error }); ok { 
  // v has OK() method 
} else { 
  // v does not have OK() method 
} 

If the v object implements the method described in the interface, ok will be true and obj will be an object on which the OK method can be called. Otherwise, ok will be false.

Note

One problem with this approach is that it hides the secret functionality from users of the code, so you must either document the function very well in order to make it clear or perhaps promote the method to its own first-class interface and insist that all objects implement it. Remember that we must always seek clear code over clever code. As a side exercise, see whether you can add the interface and use it in the decode signature instead.

We are going to add a function that will help us decode JSON request bodies and, optionally, validate the input. Create a new file called http.go and add the following code:

func decode(r *http.Request, v interface{}) error { 
  err := json.NewDecoder(r.Body).Decode(v) 
  if err != nil { 
    return err 
  } 
  if valid, ok := v.(interface { 
    OK() error 
  }); ok { 
    err = valid.OK() 
    if err != nil { 
      return err 
    } 
  } 
  return nil 
} 

The decode function takes http.Request and a destination value called v, which is where the data from the JSON will go. We check whether the OK method is implemented, and if it is, we call it. We expect OK to return nil if the object looks good; otherwise, we expect it to return an error that explains what is wrong. If we get an error, we'll return it and let the calling code deal with it.

If all is well, we return nil at the bottom of the function.

Response helpers

We are going to add a pair of helper functions that will make responding to API requests easy. Add the respond function to http.go:

func respond(ctx context.Context, w http.ResponseWriter,
 r *http.Request, v interface{}, code int) { 
  var buf bytes.Buffer 
  err := json.NewEncoder(&buf).Encode(v) 
  if err != nil { 
    respondErr(ctx, w, r, err, http.StatusInternalServerError) 
    return 
  } 
  w.Header().Set("Content-Type", 
   "application/json; charset=utf-8") 
  w.WriteHeader(code) 
  _, err = buf.WriteTo(w) 
  if err != nil { 
    log.Errorf(ctx, "respond: %s", err) 
  } 
} 

The respond method contains a context, ResponseWriter, Request, the object to respond with, and a status code. It encodes v into an internal buffer before setting the appropriate headers and writing the response.

We are using a buffer here because it's possible that the encoding might fail. If it does so but has already started writing the response, the 200 OK header will be sent to the client, which is misleading. Instead, encoding to a buffer lets us be sure that completes without issue before deciding what status code to respond with.

Now add the respondErr function at the bottom of http.go:

func respondErr(ctx context.Context, w http.ResponseWriter,
 r *http.Request, err error, code int) { 
  errObj := struct { 
    Error string `json:"error"` 
  }{ Error: err.Error() } 
  w.Header().Set("Content-Type", "application/json; charset=utf-8") 
  w.WriteHeader(code) 
  err = json.NewEncoder(w).Encode(errObj) 
  if err != nil { 
    log.Errorf(ctx, "respondErr: %s", err) 
  } 
} 

This function writes error wrapped in a struct that embeds the error string as a field called error.

Parsing path parameters

Some of our API endpoints will need to pull IDs out of the path string, but we don't want to add any dependencies to our project (such as an external router package); instead, we are going to write a simple function that will parse path parameters for us.

Let's first write a test that will explain how we want our path parsing to work. Create a file called http_test.go and add the following unit test:

func TestPathParams(t *testing.T) { 
  r, err := http.NewRequest("GET", "1/2/3/4/5", nil) 
  if err != nil { 
    t.Errorf("NewRequest: %s", err) 
  } 
  params := pathParams(r, "one/two/three/four") 
  if len(params) != 4 { 
    t.Errorf("expected 4 params but got %d: %v", len(params), params) 
  } 
  for k, v := range map[string]string{ 
    "one":   "1", 
    "two":   "2", 
    "three": "3", 
    "four":  "4", 
  } { 
    if params[k] != v { 
      t.Errorf("%s: %s != %s", k, params[k], v) 
    } 
  } 
  params = pathParams(r, "one/two/three/four/five/six") 
  if len(params) != 5 { 
    t.Errorf("expected 5 params but got %d: %v", len(params), params) 
  } 
  for k, v := range map[string]string{ 
    "one":   "1", 
    "two":   "2", 
    "three": "3", 
    "four":  "4", 
    "five":  "5", 
  } { 
    if params[k] != v { 
      t.Errorf("%s: %s != %s", k, params[k], v) 
    } 
  } 
} 

We expect to be able to pass in a pattern and have a map returned that discovers the values from the path in http.Request.

Run the test (with go test -v) and note that it fails.

At the bottom of http.go, add the following implementation to make the test pass:

func pathParams(r *http.Request,pattern string) map[string]string{ 
  params := map[string]string{} 
  pathSegs := strings.Split(strings.Trim(r.URL.Path, "/"), "/") 
  for i, seg := range strings.Split(strings.Trim(pattern, "/"), "/") { 
    if i > len(pathSegs)-1 { 
      return params 
    } 
    params[seg] = pathSegs[i] 
  } 
  return params 
} 

The function breaks the path from the specific http.Request and builds a map of the values with keys taken from breaking the pattern path. So for a pattern of /questions/id and a path of /questions/123, it would return the following map:

questions: questions
id:        123

Of course, we'd ignore the questions key, but id will be useful.

Exposing functionality via an HTTP API

Now we have all the tools we need in order to put together our API: helper functions to encode and decode data payloads in JSON, path parsing functions, and all the entities and data access functionality to persist and query data in Google Cloud Datastore.

HTTP routing in Go

The three endpoints we are going to add in order to handle questions are outlined in the following table:

HTTP request

Description

POST /questions

Ask a new question

GET /questions/{id}

Get the question with the specific ID

GET /questions

Get the top questions

Since our API design is relatively simple, there is no need to bloat out our project with an additional dependency to solve routing for us. Instead, we'll roll our own very simple adhoc routing using normal Go code. We can use a simple switch statement to detect which HTTP method was used and our pathParams helper function to see whether an ID was specified before passing execution to the appropriate place.

Create a new file called handle_questions.go and add the following http.HandlerFunc function:

func handleQuestions(w http.ResponseWriter, r *http.Request) { 
  switch r.Method { 
  case "POST": 
    handleQuestionCreate(w, r) 
  case "GET": 
    params := pathParams(r, "/api/questions/:id") 
    questionID, ok := params[":id"] 
    if ok { // GET /api/questions/ID 
      handleQuestionGet(w, r, questionID) 
      return 
    } 
    handleTopQuestions(w, r) // GET /api/questions/ 
  default: 
    http.NotFound(w, r) 
  } 
} 

If the HTTP method is POST, then we'll call handleQuestionCreate. If it's GET, then we'll see whether we can extract the ID from the path and call handleQuestionGet if we can, or handleTopQuestions if we cannot.

Context in Google App Engine

If you remember, all of our calls to App Engine functions took a context.Context object as the first parameter, but what is that and how do we create one?

Context is actually an interface that provides cancelation signals, execution deadlines, and request-scoped data throughout a stack of function calls across many components and API boundaries. The Google App Engine SDK for Go uses it throughout its APIs, the details of which are kept internal to the package, which means that we (as users of the SDK) don't have to worry about it. This is a good goal for when you use Context in your own packages; ideally, the complexity should be kept internal and hidden.

Note

You can, and should, learn more about Context through various online resources, starting with the Go Concurrency Patterns: Context blog post at https://blog.golang.org/context.

To create a context suitable for App Engine calls, you use the appengine.NewContext function, which takes http.Request as an argument to which the context will belong.

Underneath the routing code we just added, let's add the handler that will be responsible for creating a question, and we can see how we will create a new context for each request:

func handleQuestionCreate(w http.ResponseWriter, r *http.Request) { 
  ctx := appengine.NewContext(r) 
  var q Question 
  err := decode(r, &q) 
  if err != nil { 
    respondErr(ctx, w, r, err, http.StatusBadRequest) 
    return 
  } 
  err = q.Create(ctx) 
  if err != nil { 
    respondErr(ctx, w, r, err, http.StatusInternalServerError) 
    return 
  } 
  respond(ctx, w, r, q, http.StatusCreated) 
} 

We create Context and store it in the ctx variable, which has become somewhat an accepted pattern throughout the Go community. We then decode our Question (which, due to the OK method, will also validate it for us) before calling the Create helper method that we wrote earlier. Every step of the way, we pass our context along.

If anything goes wrong, we make a call out to our respondErr function, which will write out the response to the client before returning and exiting early from the function.

If all is well, we respond with Question and a http.StatusCreated status code (201).

Decoding key strings

Since we are exposing the datastore.Key objects as the id field in our objects (via the json field tags), we expect users of our API to pass back these same ID strings when referring to specific objects. This means that we need to decode these strings and turn them back into datastore.Key objects. Luckily, the datastore package provides the answer in the form of the datastore.DecodeKey function.

At the bottom of handle_questions.go, add the following handle function to get a single question:

func handleQuestionGet(w http.ResponseWriter, r *http.Request,
 questionID string) { 
  ctx := appengine.NewContext(r) 
  questionKey, err := datastore.DecodeKey(questionID) 
  if err != nil { 
    respondErr(ctx, w, r, err, http.StatusBadRequest) 
    return 
  } 
  question, err := GetQuestion(ctx, questionKey) 
  if err != nil { 
    if err == datastore.ErrNoSuchEntity { 
      respondErr(ctx, w, r, datastore.ErrNoSuchEntity,
       http.StatusNotFound) 
      return 
    } 
    respondErr(ctx, w, r, err, http.StatusInternalServerError) 
    return 
  } 
  respond(ctx, w, r, question, http.StatusOK) 
} 

After we create Context again, we decode the question ID argument to turn the string back into a datastore.Key object. The question ID string is passed in from our routing handler code, which we added at the top of the file.

Assuming question ID is a valid key and the SDK was successfully able to turn it into datastore.Key, we call our GetQuestion helper function to load Question. If we get the datastore.ErrNoSuchEntity error, then we respond with a 404 (not found) status; otherwise, we'll report the error with a http.StatusInternalServerError code.

Tip

When writing APIs, check out the HTTP status codes and other HTTP standards and see whether you can make use of them. Developers are used to them and your API will feel more natural if it speaks the same language.

If we are able to load the question, we call respond and send it back to the client as JSON.

Next, we are going to expose the functionality related to answers via a similar API to the one we used for questions:

HTTP request

Description

POST /answers

Submit an answer

GET /answers

Get the answers with the specified question ID

Create a new file called handle_answers.go and add the routing http.HandlerFunc function:

func handleAnswers(w http.ResponseWriter, r *http.Request) { 
  switch r.Method { 
  case "GET": 
    handleAnswersGet(w, r) 
  case "POST": 
    handleAnswerCreate(w, r) 
  default: 
    http.NotFound(w, r) 
  } 
} 

For GET requests, we call handleAnswersGet; for POST requests, we call handleAnswerCreate. By default, we'll respond with a 404 Not Found response.

Using query parameters

As an alternative to parsing the path, you can just take query parameters from the URL in the request, which we will do when we add the handler that reads answers:

func handleAnswersGet(w http.ResponseWriter, r *http.Request) { 
  ctx := appengine.NewContext(r) 
  q := r.URL.Query() 
  questionIDStr := q.Get("question_id") 
  questionKey, err := datastore.DecodeKey(questionIDStr) 
  if err != nil { 
    respondErr(ctx, w, r, err, http.StatusBadRequest) 
    return 
  } 
  answers, err := GetAnswers(ctx, questionKey) 
  if err != nil { 
    respondErr(ctx, w, r, err, http.StatusInternalServerError) 
    return 
  } 
  respond(ctx, w, r, answers, http.StatusOK) 
} 

Here, we use r.URL.Query() to get the http.Values that contains the query parameters and use the Get method to pull out question_id. So, the API call will look like this:

/api/answers?question_id=abc123 

Tip

You should be consistent in your API in the real world. We have used a mix of path parameters and query parameters to show off the differences, but it is recommended that you pick one style and stick to it.

Anonymous structs for request data

The API for answering a question is to post to /api/answers with a body that contains the answer details as well as the question ID string. This structure is not the same as our internal representation of Answer because the question ID string would need to be decoded into datastore.Key. We could leave the field in and indicate with field tags that it should be omitted from both the JSON and the data store, but there is a cleaner approach.

We can specify an inline, anonymous structure to hold the new answer, and the best place to do this is inside the handler function that deals with that data this means that we don't need to add a new type to our API, but we can still represent the request data we are expecting.

At the bottom of handle_answers.go, add the handleAnswerCreate function:

func handleAnswerCreate(w http.ResponseWriter, r *http.Request) { 
  ctx := appengine.NewContext(r) 
  var newAnswer struct { 
    Answer 
    QuestionID string `json:"question_id"` 
  } 
  err := decode(r, &newAnswer) 
  if err != nil { 
    respondErr(ctx, w, r, err, http.StatusBadRequest) 
    return 
  } 
  questionKey, err := datastore.DecodeKey(newAnswer.QuestionID) 
  if err != nil { 
    respondErr(ctx, w, r, err, http.StatusBadRequest) 
    return 
  } 
  err = newAnswer.OK() 
  if err != nil { 
    respondErr(ctx, w, r, err, http.StatusBadRequest) 
    return 
  } 
  answer := newAnswer.Answer 
  user, err := UserFromAEUser(ctx) 
  if err != nil { 
    respondErr(ctx, w, r, err, http.StatusBadRequest) 
    return 
  } 
  answer.User = user.Card() 
  err = answer.Create(ctx, questionKey) 
  if err != nil { 
    respondErr(ctx, w, r, err, http.StatusInternalServerError) 
    return 
  } 
  respond(ctx, w, r, answer, http.StatusCreated) 
} 

Look at the somewhat unusual var newAnswer struct line. We are declaring a new variable called newAnswer, which has a type of an anonymous struct (it has no name) that contains QuestionID string and embeds Answer. We can decode the request body into this type, and we will capture any specific Answer fields as well as QuestionID. We then decode the question ID into datastore.Key as we did earlier, validate the answer, and set the User (UserCard) field by getting the currently authenticated user and calling the Card helper method.

If all is well, we call Create, which will do the work to save the answer to the question.

Finally, we need to expose the voting functionality in our API.

Writing self-similar code

Our voting API has only a single endpoint, a post to /votes. So, of course, there is no need to do any routing on this method (we could just check the method in the handler itself), but there is something to be said for writing code that is familiar and similar to other code in the same package. In our case, omitting a router might jar a little if somebody else is looking at our code and expects one after seeing the routers for questions and answers.

So let's add a simple router handler to a new file called handle_votes.go:

func handleVotes(w http.ResponseWriter, r *http.Request) { 
  if r.Method != "POST" { 
    http.NotFound(w, r) 
    return 
  } 
  handleVote(w, r) 
} 

Our router just checks the method and exits early if it's not POST, before calling the handleVote function, which we will add next.

Validation methods that return an error

The OK method that we added to some of our objects is a nice way to add validation methods to our code.

We want to ensure that the incoming score value is valid (in our case, either -1 or 1), so we could write a function like this:

func validScore(score int) bool { 
  return score == -1 || score == 1 
} 

If we used this function in a few places, we would have to keep repeating the code that explained that the score was not valid. If, however, the function returns an error, you can encapsulate that in one place.

To votes.go, add the following validScore function:

func validScore(score int) error { 
  if score != -1 && score != 1 { 
    return errors.New("invalid score") 
  } 
  return nil 
} 

In this version, we return nil if the score is valid; otherwise, we return an error that explains what is wrong.

We will make use of this validation function when we add our handleVote function to handle_votes.go:

func handleVote(w http.ResponseWriter, r *http.Request) { 
  ctx := appengine.NewContext(r) 
  var newVote struct { 
    AnswerID string `json:"answer_id"` 
    Score    int    `json:"score"` 
  } 
  err := decode(r, &newVote) 
  if err != nil { 
    respondErr(ctx, w, r, err, http.StatusBadRequest) 
    return 
  } 
  err = validScore(newVote.Score) 
  if err != nil { 
    respondErr(ctx, w, r, err, http.StatusBadRequest) 
    return 
  } 
  answerKey, err := datastore.DecodeKey(newVote.AnswerID) 
  if err != nil { 
    respondErr(ctx, w, r, errors.New("invalid answer_id"), 
    http.StatusBadRequest) 
    return 
  } 
  vote, err := CastVote(ctx, answerKey, newVote.Score) 
  if err != nil { 
    respondErr(ctx, w, r, err, http.StatusInternalServerError) 
    return 
  } 
  respond(ctx, w, r, vote, http.StatusCreated) 
} 

This will look pretty familiar by now, which highlights why we put all the data access logic in a different place to our handlers; the handlers can then focus on HTTP tasks, such as decoding the request and writing the response, and leave the application specifics to the other objects.

We have also broken down the logic into distinct files, with a pattern of prefixing HTTP handler code with handle_, so we quickly know where to look when we want to work on a specific piece of the project.

Mapping the router handlers

Let's update our main.go file by changing the init function to map the real handlers to HTTP paths:

func init() { 
  http.HandleFunc("/api/questions/", handleQuestions) 
  http.HandleFunc("/api/answers/", handleAnswers) 
  http.HandleFunc("/api/votes/", handleVotes) 
} 

You can also remove the now redundant handleHello handler function.

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

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