Suppose you want to create an augmented reality style mobile game, where users must visit real-world locations to progress through the game. Given that a mobile device has access to its current location, the game can randomly generate each location the user needs to visit, but how do you know these locations are accessible? A randomly generated location could very well be in the ocean, or some other inaccessible area.

What you need is a way to identify real-world locations that your game can randomly offer as destinations for the game. The Google Places API is a perfect fit for this, as it allows you to search for places within a particular radius at a given location. Given that the user's mobile device knows its current location, you can use the Google Places API Web Service to search for nearby places, and offer these as destinations the player must reach.

Using the Google Places API directly from a mobile device presents some interesting problems in terms of ensuring API key security, and optimising network performance. This codelab helps you address those issues by building a server-side proxy using Golang and Google App Engine. The proxy will take requests from the mobile devices, and make requests to the Google Places API on its behalf.

What you'll build

In this codelab, you'll build a Google App Engine proxy for the Google Places API web service, which will:

What you'll learn

What you'll need

Self-paced environment setup

If you don't already have a Google Account (Gmail or Google Apps), you must create one.

Sign in to Google Cloud Platform console (console.cloud.google.com) and create a new project:

Remember the project ID, as you'll use it later. The project ID is a unique name across all Google Cloud projects. The name above has already been taken and will not work for you. Insert your own project ID wherever you see YOUR_PROJECT_ID in this codelab.

Start Cloud Shell

While Google Cloud can be operated remotely from your laptop, in this codelab you'll use Cloud Shell, a command line environment running in the Cloud. Cloud Shell provides an automatically provisioned Linux virtual machine is loaded with all the development tools you need (gcloud, go, and more). It offers a persistent 5GB home directory, and runs on the Google Cloud, greatly enhancing network performance and authentication.

To activate Cloud Shell, from the Cloud platform console click the button on the top right-hand side (it should only take a few moments to provision and connect to the environment):

Clicking on the above button will open a new shell in the lower part of your browser, after possibly showing an introductory interstitial:

Once connected to the Cloud Shell, you should see that you are already authenticated and that the project is already set to your YOUR_PROJECT_ID:

$ gcloud auth list
Credentialed accounts:
 - <myaccount>@<mydomain>.com (active)
$ gcloud config list project
[core]
project = <
YOUR_PROJECT_ID
>

If for some reason the project is not set, run the following command:

$ gcloud config set project <
YOUR_PROJECT_ID
>

Starting developing with Hello World

In your Cloud Shell instance, you'll start by creating a Go App Engine app that will serve as the basis for the rest of the codelab.

In the toolbar of the Cloud Shell, click on the file icon to open a menu, and then select Launch code editor to open a code editor in a new tab. This web based code editor allows you to easily edit files in the Cloud Shell instance.

Create a new places-proxy directory for your application in the code editor, by opening the File menu, and selecting New > Folder.

Name the new folder places-proxy:

Next you'll create a small Go App Engine app to make sure everything's working. Hello World!

Create a file in the places-proxy directory named app.yaml. Put the following content in the app.yaml file:

app.yaml

runtime: go
api_version: go1

handlers:
- url: /.*
  script: _go_app
  login: admin

This configuration file configures your App Engine app to use the Golang Standard run time. For background information on the meaning of the configuration items in this file, see the Google App Engine Go Standard Environment Documentation.

Next, create a proxy.go file alongside the app.yaml file:

proxy.go

package proxy

import (
    "fmt"
    "net/http"
)

func init() {
    http.HandleFunc("/", handler)
}

func handler(w http.ResponseWriter, r *http.Request) {
    fmt.Fprint(w, "Hello, world!")
}

It is worth pausing a moment here to understand what this code does, at least at a high level. You have defined a package proxy that registers a handler function for HTTP requests matching the path "/" in the init function, which is called by the Golang App Engine runtime at startup.

The handler function, handily called handler, simply writes out the text string "Hello, world!" when called. This text will be relayed back to your browser, where you will be able to read it. In future steps you'll extend this further into your Places API proxy.

After carrying out these steps, you should now have an editor that looks like this:

To test this application, you can run the App Engine development server inside the Cloud Shell instance. Go back to the Cloud Shell command line, and type the following:

$ cd places-proxy
$ goapp serve

You will see some lines of log output showing you that you are indeed running the development server on the Cloud Shell instance, with the hello world web app listening on localhost port 8080. You can open a web browser tab on this app by selecting the Preview on port 8080 menu item from the Cloud Shell toolbar.

Clicking on this menu item will open a new tab in your web browser with the "Hello, world!" message served from the App Engine development server.

In the next step you'll extend this code to make requests to the Google Places API and return the results back to the browser, forming the basis of your proxy.

Your next step is to create a basic working proxy that makes requests to the Google Places API, and returns the data to the browser (or mobile application) that made the request to the proxy. With this in place, you will have implicitly achieved one of the goals of this codelab, which is to prevent the Google Maps API key from being exposed directly to the mobile application.

API keys for Google Maps Web Services are intended for use only in server-side applications, and are not restricted to specific websites or mobile applications. Embedding these keys directly into a mobile application could give a malicious user the opportunity to extract the API key and abuse it. When you communicate with the Google Maps Web Services through a proxy, you can protect the API key by embedding it within the proxy server.

First, edit proxy.go again to do the following:

proxy.go

package proxy

import (
    "appengine"
    "appengine/urlfetch"
    "fmt"
    "io/ioutil"
    "net/http"
)

// Define constants for the API key and the web service URL.
const placesAPIKey = "YOUR_API_KEY"
const placesURL = "https://maps.googleapis.com/maps/api/place/nearbysearch/json?key=%s&location=%s&radius=%s"

// Makes the request to the Places API.
func fetchPlaces(ctx appengine.Context, location, radius string) ([]byte, error) {
    client := urlfetch.Client(ctx)
    resp, err := client.Get(fmt.Sprintf(placesURL, placesAPIKey, location, radius))
    if err != nil {
        return nil, err
    }
    defer resp.Body.Close()
    return ioutil.ReadAll(resp.Body)
}

// Calls the fetchPlaces function and returns the results to the browser.
func handler(w http.ResponseWriter, r *http.Request) {
    ctx := appengine.NewContext(r)
    places, err := fetchPlaces(ctx, r.FormValue("location"), r.FormValue("radius"))
    if err != nil {
        http.Error(w, err.Error(), http.StatusInternalServerError)
        return
    }
    w.Header().Add("Content-Type", "application/json; charset=utf-8")
    w.Write(places)
}

func init() {
    http.HandleFunc("/", handler)
}

There is one quick diversion you need to make here, to replace the YOUR_API_KEY in the above source with an actual Google Maps API key. Follow these steps to get an API key:

  1. Go to the Google API Console. (Use this link, because it has some smarts to enable the correct APIs for you.)
  2. Select the project you created earlier in this code lab.
  3. Click Continue to enable the API and any related services.
  4. On the Credentials page, get an API key. Don't worry about adding restrictions to this API Key for the purposes of this codelab.
  5. Copy the API Key and replace YOUR_API_KEY with it in the proxy.go file (leaving the "quotes" around it).

Back in the Cloud Shell, restart your application by pressing Ctrl and C to stop it, and running the goapp serve command again. You'll need to restart the application each time you make a change to the source code.

Once the application has restarted, go back to the browser window that originally displayed the "Hello, World!" message (or use the web preview again to open a new browser window), and reload the page. You should see the following:

What's happening here? The good news is you're displaying exactly the response the Google Places API is sending you - proxy success! The bad news is that the response is an error. This is because you haven't specified a location and radius to use in the search. If you study the handler function, you'll see it expects the location and radius to be specified as part of the URL used to request the proxy, and it then passes those on to the Places API as arguments to the fetchPlaces function.

Let's go back to the browser window showing the error, and append a location and radius to it. For example:

https://8080-dot-2051896-dot-devshell.appspot.com/?authuser=0&location=-33.8594663,151.1927602&radius=500

Now you should see lots of data for nearby places, returned by the Google Places API:

As you can see from the output of your proxy, the Google Places API returns an extensive amount of data for each place in the response, including photos, opening hours, classifications, and more. Our supposed augmented reality game doesn't need any of this information, as it's only interested in the latitude and longitude coordinates of the locations. Your next step will be to enhance the proxy, by having it strip out the extraneous data provided by the Google Places API, so that it returns just the location coordinates to the mobile device. Reducing the amount of data being sent to the mobile application is an important consideration, given the inconsistent capacity of mobile networks.

Edit proxy.go again. The code remains mostly intact, with some additions:

Remember to replace YOUR_API_KEY with your own API key after copying the code into your proxy.go file.

proxy.go

package proxy

import (
    "appengine"
    "appengine/urlfetch"
    "encoding/json"
    "fmt"
    "net/http"
    "io"
)

const placesAPIKey = "YOUR_API_KEY"
const placesURL = "https://maps.googleapis.com/maps/api/place/nearbysearch/json?key=%s&location=%s&radius=%s"

// Represents the structure of the Places API JSON response.
type placeResults struct {
    Results []struct {
        Geometry struct {
            Location struct {
                Lat float64 `json:"lat"`
                Lng float64 `json:"lng"`
            } `json:"location"`
        } `json:"geometry"`
    } `json:"results"`
}

// Takes the JSON response from the Google Places API, and converts it into a variable of type placeResult
func formatPlaces(body io.Reader)([]byte, error) {
    var places placeResults
    if err := json.NewDecoder(body).Decode(&places); err != nil {
        return nil, err
    }
    return json.Marshal(places)
}

func fetchPlaces(ctx appengine.Context, location, radius string) ([]byte, error) {
    client := urlfetch.Client(ctx)
    resp, err := client.Get(fmt.Sprintf(placesURL, placesAPIKey, location, radius))
    if err != nil {
        return nil, err
    }
    defer resp.Body.Close()
    // Change the return value to use the new formatPlaces function.
    return formatPlaces(resp.Body)
}

func handler(w http.ResponseWriter, r *http.Request) {
    ctx := appengine.NewContext(r)
    places, err := fetchPlaces(ctx, r.FormValue("location"), r.FormValue("radius"))
    if err != nil {
        http.Error(w, err.Error(), http.StatusInternalServerError)
        return
    }
    w.Header().Add("Content-Type", "application/json; charset=utf-8")
    w.Write(places)
}

func init() {
    http.HandleFunc("/", handler)
}

Restart the application, then reload the browser window with the URL that contains the location and radius parameters. You should see a much lighter version of the data returned from the Google Places API:

Magic! But how did it happen? We took advantage of the way Golang converts strings of JSON into typed data. If you define only the fields in the JSON that you're interested in, such as lat and lng, Golang conveniently ignores all the other data. The formatPlaces function simply converts a JSON string into a typed variable, and back into a JSON string again, all for the sake of stripping down the data.

Suppose several people near each other are playing your augmented reality game. The data returned from the Google Places API will be very similar for each player, if not identical. It therefore makes sense for your proxy to store a copy of the places returned for a given location in a cache that it can then use to retrieve places for subsequent requests for the same (or a very nearby) location, which will speed up your proxy server significantly.

Google App Engine gives you access to Memcache, a very fast caching service, purposely suited to this type of task. Memcache allows you to store values by a given key, and then retrieve them again using that same key. For caching places by location, you'll use the location as the key for storing the response from the Google Places API. This presents a slight problem though, as most locations will be very unique, since an increasing number of decimal places in a latitude and longitude can become extremely precise. For example, a latitude or longitude with 9 decimal places indicates a degree of accuracy down to 110 microns, which is about the width of two human hairs, while 4 decimal places indicates a lesser degree of accuracy of around 11 metres. If each request to your proxy used a latitude and longitude with 9 decimal places, every location would be entirely unique, and looking up your cached responses would never retrieve a value. To address this problem, you can round off the latitude and longitude in each request's location to 4 decimal places, and use the rounded location as your cache key.

Edit proxy.go to do the following::

Remember to replace YOUR_API_KEY with your own API key.

proxy.go

package proxy

import (
    "appengine"
    "appengine/memcache"
    "appengine/urlfetch"
    "encoding/json"
    "errors"
    "fmt"
    "net/http"
    "strconv"
    "strings"
    "time"
    "io"
)

// New constants for location rounding and cache expiry.
const locationPrecision = 4  // Up to 11 metres
const cacheExpiry = time.Second * 600  // 5 minutes
const placesAPIKey = "YOUR_API_KEY"
const placesURL = "https://maps.googleapis.com/maps/api/place/nearbysearch/json?key=%s&location=%s&radius=%s"

type placeResults struct {
    Results []struct {
        Geometry struct {
            Location struct {
                Lat float64 `json:"lat"`
                Lng float64 `json:"lng"`
            } `json:"location"`
        } `json:"geometry"`
    } `json:"results"`
}

// Rounds off the latitude and longitude of a location.
func normalizeLocation(location string) (string, error) {
    var lat, lng float64
    var err error
    latLng := strings.Split(location, ",")
    if len(latLng) != 2 {
        return "", errors.New("Invalid location")
    }
    if lat, err = strconv.ParseFloat(latLng[0], locationPrecision); err != nil {
        return "", errors.New("Invalid location")
    }
    if lng, err = strconv.ParseFloat(latLng[1], locationPrecision); err != nil {
        return "", errors.New("Invalid location")
    }
    return fmt.Sprintf("%.2f,%.2f", lat, lng), nil
}

func formatPlaces(body io.Reader)([]byte, error) {
    var places placeResults
    if err := json.NewDecoder(body).Decode(&places); err != nil {
        return nil, err
    }
    return json.Marshal(places)
}

// fetchPlaces now stores results in the cache.
func fetchPlaces(ctx appengine.Context, location, radius string) ([]byte, error) {
    client := urlfetch.Client(ctx)
    resp, err := client.Get(fmt.Sprintf(placesURL, placesAPIKey, location, radius))
    if err != nil {
        return nil, err
    }
    defer resp.Body.Close()
    places, err := formatPlaces(resp.Body)
    if (err == nil) {
        memcache.Set(ctx, &memcache.Item{
            Key:        location,
            Value:      places,
            Expiration: cacheExpiry,
        })
    }
    return places, err
}

// handler now retrieves results from the cache if they exist.
func handler(w http.ResponseWriter, r *http.Request) {
    radius := r.FormValue("radius")
    location, err := normalizeLocation(r.FormValue("location"))
    if err != nil {
        http.Error(w, err.Error(), http.StatusInternalServerError)
        return
    }
    ctx := appengine.NewContext(r)
    var places []byte
    if cached, err := memcache.Get(ctx, location); err == nil {
        places = cached.Value
        // We use Golang's goroutines here to call fetchPlaces in the background, 
        // without having to wait for the result it returns. This ensures that 
        // both the cache remains fresh, and that we also remain in compliance 
        // with the Google Places API Policies.
        go fetchPlaces(ctx, location, radius)
    } else if places, err = fetchPlaces(ctx, location, radius); err != nil {
        http.Error(w, err.Error(), http.StatusInternalServerError)
        return
    }
    w.Header().Add("Content-Type", "application/json; charset=utf-8")
    w.Write(places)
}

func init() {
    http.HandleFunc("/", handler)
}

Restart the application, and then reload the browser window with the URL that includes the location and radius parameters. You should see exactly the same results when you change the digits in the 5th or 6th decimal places of the latitude and longitude in the URL.

Next up you'll deploy your proxy to Google App Engine, which will also enable you to verify that requests are being cached, by viewing the dashboard for Memcache.

Now that you have built an web app that you are proud of, it's time to show it off to the world. You need to automatically scale the back end, based on the number of concurrent incoming requests. Thankfully, Google App Engine handles this effortlessly. All you need to do is deploy the app.

Back in the Google Cloud Shell, stop the application again and type the following command. When asked which region to create the App Engine app in, select us-central.

$ gcloud app deploy 
You are creating an app for project [places-mobile-proxy].
WARNING: Creating an app for a project is irreversible.
Please choose a region for your application. After choosing a region, 
you cannot change it. Which region would you like to choose?
 [1] europe-west   (supports standard)
 [2] us-central    (supports standard and flexible)
 [3] us-east1      (supports standard and flexible)
 [4] asia-northeast1 (supports standard and flexible)
 [5] cancel
Please enter your numeric choice:  2
Creating App Engine application in project [places-mobile-proxy] and region [us-central]....done.                                                                                    
You are about to deploy the following services:
 - places-mobile-proxy/default/20170213t155013 (from [/home/stephenmcd/places-proxy/app.yaml])
     Deploying to URL: [https://places-mobile-proxy.appspot.com]
Do you want to continue (Y/n)?  y
Beginning deployment of service [default]...
File upload done.
Updating service [default]...done.                                                                                               
Deployed service [default] to [https://places-mobile-proxy.appspot.com]
You can read logs from the command line by running:
  $ gcloud app logs read -s default
To view your application in the web browser run:
  $ gcloud app browse

Open a new browser tab on the URL that the service was deployed to. In the above listing, the deployed URL (after adding the location and radius parameters) is https://places-mobile-proxy.appspot.com?location=-33.8594663,151.1927602&radius=500, but yours will be different.

Cleanup

The easiest way to clean up all the resources created in this project is to shut down the Google Cloud Project that you created at the start of this tutorial:

Congratulations, you have successfully completed this codelab! If you enjoyed this codelab, but would like to dive into the code some more, please have a look at our source code repository at https://github.com/googlecodelabs/google-maps-web-services-proxy.