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.
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.
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.
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
.
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.
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.
The three endpoints we are going to add in order to handle questions are outlined in the following table:
HTTP request |
Description |
|
Ask a new question |
|
Get the question with the specific ID |
|
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.
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.
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).
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.
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 |
|
Submit an answer |
|
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.
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
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.
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.
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.
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.
3.14.144.216