In this post I will explain how to build a nice and working RESTful API for our weather service using only the Go standard library. If you missed the first part of this series, I recommend reading it before continuing here.

You can find the codebase of this introductory series on GitHub.

Extending the blueprint

Well, we have a nice blueprint of the handlers we want, but shoving around global variables is neither fun nor good style. So we will extend each of our handlers to use Go’s support of higher-order functions, that allows us to use functions as values. We will do this for each of our handlers. Our final result should look something like this. Notice the changes in the main method when registering the handlers.

// import ...

func listCitiesHandler(service weather.Service) http.HandlerFunc {
    return func(w http.ResponseWriter, r *http.Request) {
	    fmt.Fprintln(w, "/city")
    }
}

func showTemperatureHandler(service weather.Service) http.HandlerFunc {
    return func(w http.ResponseWriter, r *http.Request) {
        fmt.Fprintln(w, "/city/")
    }
}

func sendReportHandler(service weather.Service) http.HandlerFunc {
    return func(w http.ResponseWriter, r *http.Request) {
        fmt.Fprintln(w, "/report")
    }
}

func main() {
    service := weather.NewInMemoryService()
    http.HandleFunc("/city", listCitiesHandler(service))
    http.HandleFunc("/city/", showTemperatureHandler(service))
    http.HandleFunc("/report", sendReportHandler(service))
    // if err := ...
    // ...
}

Listing the stations

Let’s focus on the first handler on our blueprint, the city listing. To remind you of our handler description, we want something like the following.

Example: Requesting a list of stations

GET /city => {
    "munich",
    "zurich",
    "oberjoch"
}

When we look up the documentation of the weather package we imported, we will learn that calling service.Cities would give us a slice of city names. But since we want the list in a JSON format, we will have to encode it. Luckily, Go provides a dead-simple package for serializing and deserializing structures from/to JSON called encoding/json.

We can divide our job to write the list of cities to the HTTP response into three basic steps:

This literally translates into the following Go code.

cities := service.Cities()
json.NewEncoder(w).Encode(cities)

Since this is all of the needed business logic in our listCitiesHandler handler, we are pretty much done here except for error handling.

func listCitiesHandler(service weather.Service) http.HandleFunc {
    return func(w http.ResponseWriter, r *http.Request) {
        if r.Method != "GET" {
            http.Error(w, "method not allowed", http.StatusMethodNotAllowed)
            return
        }
        cities := service.Cities()
        encoder := json.NewEncoder(w)
        json.NewEncoder(w).Encode(cities)
    }
}

Luckily, again, Go provides another set of comfortable methods and constants to propagate errors back to the user using HTTP. The first case to handle is when the user does request our listCitiesHandler service using the wrong HTTP method (something absurd like POST) and since we provide a strict REST API we shall not honor these fools with a valid response. To show them the bad things they have done, we respond instead with a nice method not allowed status and text using the http.Error method. It accepts a ResponseWriter, a plain text error message and the HTTP error code as parameters.

A similar thing happens at the end of our handler when we fail on ourselves by somehow being not able to properly encode a list of strings.

Accessing temperature data

Our API users are now able to access a list of cities which have reported temperatures associated with. Well, of course, the user wants to know the current temperature of a specific station. This is exactly, what our showTemperatureHandler has to do.

  1. Receive a station parameter from the user
  2. Lookup station data
  3. Convert to requested unit
  4. Print out temperature

This is a rather massive procedure, but we can easily decompose it into subproblems. First of all, how can we take a station or city parameter from the user? We can simply use the HTML URI for that one!

city := strings.TrimPrefix(r.URL.Path, "/city/")

Using URL.Path we can get the request path and strip away the unwanted path prefix with the standard library method strings.StripPrefix. Since the unit of measurement is still missing and we already know how to extract data from web URIs we chould choose to do the same for our optional parameter. But careful: this would be extremely imprecise! We rather want to use a URI query parameter for our case.

unit := r.URL.Query()["unit"]
if unit == nil {
    // provide default value
    unit = []string{"celsius"}
}

We now got a string describing our unit of measurement, but we need to convert it into a discrete constant.

This way, we can easily provide a default value in case the tag is not provided and make use of the symbolic language of a question mark, visually marking the tag as optional.

Parsing the temperature unit

The rest of the handler is very similar to the routine of the listCitiesHandler but instead of serializing a struct from a library, we will convert the temperature using a method called parseTemperatureUnit and use a custom response struct.

func parseTemperatureUnit(unit string) (weather.Unit, bool) {
	switch strings.ToLower(unit) {
	case "kelvin":
		return weather.Kelvin, true
	case "celsius":
		return weather.Celsius, true
	case "fahrenheit":
		return weather.Fahrenheit, true
	}
	return weather.Kelvin, false
}

We use a standard Go switch statement to match the unit. If we are not able to find a perfect match, we default to using Kelvin units. Now we can finally parse our unit parameter and fetch the report from the datastore!

requestedUnit, ok := parseTemperatureUnit(unit[0])
if !ok {
    http.Error(w, "unknown unit of measurement", http.StatusBadRequest)
    return
}
temp, err := service.TemperatureIn(city, requestedUnit)
if err != nil {
    http.Error(w, "city not found", http.StatusNotFound)
    return
}