JSON rendering is a very easy task in Go. It is one thing to render JSON, it is another to set the structure to handle data, errors, links, metadata and other necessary information. JSON-API is one of the standards made to solve this issue and we are going to use it alongside our framework to create an API.
JSON doesn't force you to use any structure and without a structure (your own or an established one) you are sure to have some inconsistencies at one point in your API. A basic example, I often see plain text errors in JSON APIs and it's just wrong, the client is expecting a certain content type and the response is not what is expected. In addition HTTP statuses are often misinterpreted and 2 different APIs might end up using the same status for different types of errors resulting in headaches for the users. By using a standard, we don't spend time making our own structure and can focus on making user-friendly APIs.
We are going to build an API to manage teas, a simple REST API to create, retrieve, update and delete teas. It can serve as a foundation for an mobile tasting application or to manage items in an eshop. And we are going to use MongoDB to not deal with DB schemas and migrations because it's not the subject of this article.
The example from the previous article (you can find a gist of the code here) already included a DB connection but was fake just for the purpose of the article, so let's replace it with a real DB now:
func main() {
session, err := mgo.Dial("localhost")
if err != nil {
panic(err)
}
defer session.Close()
session.SetMode(mgo.Monotonic, true)
appC := appContext{session.DB("test")}
commonHandlers := alice.New(context.ClearHandler, loggingHandler, recoverHandler)
router := httprouter.New()
router.Get("/teas/:id", commonHandlers.ThenFunc(appC.teaHandler))
http.ListenAndServe(":8080", router)
}
We use mgo for the MongoDB driver, don't forget to get it and import it:
go get gopkg.in/mgo.v2
The handler to find a tea looked like that:
func (c *appContext) teaHandler(w http.ResponseWriter, r *http.Request) {
params := context.Get(r, "params").(httprouter.Params)
tea := getTea(c.db, params.ByName("id"))
json.NewEncoder(w).Encode(tea)
}
It's a non-working example so let's make it workable.
Like I said above, rendering JSON is easy. We pass anything to the json
package and it tries to convert it to JSON. The structure of a tea would be like:
type Tea struct {
Id int `json:"id"`
Name string `json:"name"`
Category string `json:"category"`
}
JSON-API standard however is a bit less easy but it's not hard either. The JSON response must have a top-level key containing document and its name is the collection's name (teas in our case) but it can also be data
so let's use data
. This top-level element will have a individual tea as its value. It would translate to a simple struct like:
type TeaResource struct {
Data Tea `json:"data"`
}
And we can now update our teaHandler
to encode our found tea to the appropriate structure:
func (c *appContext) teaHandler(w http.ResponseWriter, r *http.Request) {
params := context.Get(r, "params").(httprouter.Params)
// tea := getTea(c.db, params.ByName("id"))
w.Header.Set("Content-Type", "application/vnd.api+json") // We must set the appropriate Content-Type.
json.NewEncoder(w).Encode(TeaResource{tea})
}
To finish the handler, we need to replace the fake DB access by the real one. One way to do it is to create a repository that mediates between the entity TeaResource
and the DB.