Create a Restaurant Finder

Using the http package, parse JSON from the Yelp API and display it to find restaurants in the Go language

July 19, 2020

0 views


Interested in this project?

Continue Learning

Introduction

Dependencies

It would be beneficial to have a basic understanding of HTML and CSS before starting this tutorial. If you are comfortable with the concepts in the To-Do List App, you should be more than fine for this tutorial!

What is Go?

Go or Golang is a language created by Google. Rumor has it that it was conceived by Google developers when they were waiting for the code compilation to complete in a project.

The reason that Go is special is mostly because of its speed. It compiles and runs very quickly. It also has many features that allows processes to run concurrently (known as goroutines).

When should I use Go?

Go should be used for anything that requires speed and has precise memory or hardware requirements. It is most similar to C++ but it has more functionality. Additionally, with Go you are highly unlikely to run into errors while its running because it pre-checks all of your code (it is a compiled language).

You might use Go over Python or JavaScript for web development if you are looking for speed or require high amounts of processing.

Getting Started and Set Up

Setup Go

Download the Go tools here: https://golang.org/doc/install. Next, you will want to download VSCode if you haven't already. VSCode has extensions that make golang development very nice.

In VSCode, click the icon on the left that looks like building blocks as shown in this photo.

VSCode Extensions

Search for "Go" and install the first extension you see. It should be called "Go". This gives language support so that you can start coding and get highlighting to help you find out when something is wrong.

Now you are all set up!

Starting with the Webpage

Create the Webpage

We will be building a web app in which you can search for restaurants near you with the Yelp API!

Start by creating a file: index.html. This will be the template for your main page. Let's add the boilerplate code for the webpage:

<!DOCTYPE html>
<html>
  <head>
    <meta charset="UTF-8" />
    <meta name="viewport" content="width=device-width, initial-scale=1" />
    <title>Golang Restaurant Finder</title>
  </head>
  <body>
    <h1>Find a Restaurant Here</h1>
    </body>
</html>

Serve the Webpage

Now let's get this file on a server. A server is basically somewhere on the internet that files can reside, allowing them to communicate with the internet. When you open an html page in your browser without a server, it cannot actually communicate with other computers / servers.

Create a main.go file. We must create a template out of the index.html file that Go can display.

package main

import (
    "fmt"
    "html/template"
    "net/http"
)

var tpl = template.Must(template.ParseFiles("index.html")) // creates the index.html page

func indexHandler(w http.ResponseWriter, r *http.Request) {
    tpl.Execute(w, nil) // display template
}

func main() {
    fmt.Println("App Started")
    
    mux := http.NewServeMux() // helps to call the correct handler based on the URL

    mux.HandleFunc("/", indexHandler) // what function to call on main page
    http.ListenAndServe(":3000", mux) // start a local server to run files
}

Let's break this down.

The general way that Go works (in this context) is similar to Flask or Django, if you are familiar with it. Based on the url, Go will execute different functions.

First off, to go over some of the basics, variables in Go have the type (int, bool, etc.) after the name. As you may notice in the indexHandler parameters (arguments), w is the name and http.ResponseWriter is the type. Additionally, variables can be defined in different ways:

var a int = 10

This creates a variable called a with a type of int.

var b = 10

This creates a variable called b with a type of int

b is defined in the same way that tpl is defined.

c := 10

This creates a variable called c with a type of int.

c is defined in the same way that mux is defined. This declaration is specific to Go. It is called the shorthand variable creator. It automatically assigns a type to c.

var d int
d = 10

This creates a variable called d with a type of int.

As you can see, all three methods create the same variable.

Secondly, we must import some libraries from the standard library to help us with the project. We are mainly using the http library for this project. The fmt library helps us print to the command line.

Now, to go over the code, we start func main() with a print statement that shows up in the command line telling us that our App is running. Then, we use mux := http.NewServeMux() to create what's called an HTTP request multiplexer . This is fancy talk for something that helps to handle which function to call based on the URL. Then we tell mux to call indexHandler on the main page. Finally we start our local server so that it can "serve" the files.

In indexHandler we execute and display the template.

At the very top of our file we see the definition of tpl. This variable creates the template that displays content on the page. the template.Must() is there so that if the template is not able to render, the app crashes and errors. We want this to happen because without this template we have nothing to show the user. The template.ParseFiles() takes index.html and interprets it.

Now run

go run main.go

In your command line where your files are (or press F5 in VSCode) and go to localhost:3000 in your browser. You should see something like this!

First Local Server

Congrats! Your code is being hosted and displayed on a local server!

Styles

Now let's get some styles going. If we want our styles to show up, we need a styles.css file to also be on our local server.

Create a folder called assets. Inside this folder, create a file called styles.css.

Copy and paste these styles into that file. I thought these styles looked fairly nice, but there is tons of room for improvement. Feel free to change things up as you go. These are all the styles needed for the entire project.

.restaurantCard {
  max-width: 250px;
  text-align: left;
  margin: auto;
  padding: 0;
  border: 1px solid #999;
  margin-bottom: 20px;

}

a {
  color: black;
  text-decoration: none;
  cursor: pointer;
}

input {
  margin-bottom: 20px;
  outline: none;
  border: 1px solid #999;
  padding: 5px 10px;
}

img {
  max-width: 250px;
  padding: 0;
  margin: 0;
}

h3, p {
  margin-left: 10px;
}

body {
  text-align: center;
  font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, Ubuntu, Cantarell, 'Open Sans', 'Helvetica Neue', sans-serif;
}

Now, let's get a file server running. This will allow us to make our styles accessible from our index.html file. Change your main.go file to this:

package main

import (
    "fmt"
    "html/template"
    "net/http"
)

var tpl = template.Must(template.ParseFiles("index.html")) // creates the index.html page

func indexHandler(w http.ResponseWriter, r *http.Request) {
    tpl.Execute(w, nil) // create template
}

func main() {
    fmt.Println("App Started")

    mux := http.NewServeMux() // helps to call the correct handler based on the URL

    fs := http.FileServer(http.Dir("assets")) // put the "static" files on the server like the CSS file
    mux.Handle("/assets/", http.StripPrefix("/assets/", fs))

    mux.HandleFunc("/", indexHandler) // what function to call on main page
    http.ListenAndServe(":3000", mux) // start a local server to run files
}

Now we have created fs, an http.FileServer, which is something that puts everything in our assets folder (i.e. styles.css) onto our local server. Reminder -- a local server is something that can understand URLs and communicate with the internet.

Awesome now do

go run main.go

In your command line where your files are (or press F5 in VSCode) and go to localhost:3000 in your browser. You should see something like this:

Golang server after styles

The text is now centered and the font should have changed! If this doesn't seem like as big of a change as expected, know that many of the styles will help make other elements look better.

If you ever encounter trouble with the styles updating, it is likely that the browser has "cached" the previous version of the file. To fix this, go to localhost:3000/assets and click on styles.css. You should see your styles file. Refresh the page and the new styles should update. Return to localhost:3000 and you're good to go.

Create the form to Search

Now let's create the form on our webpage so that we can search for the restaurants we want. Change your index.html file to this:

<!DOCTYPE html>
<html>
  <head>
    <meta charset="UTF-8" />
    <meta name="viewport" content="width=device-width, initial-scale=1" />
    <title>Golang Restaurant Finder</title>
    <link rel="stylesheet" href="assets/styles.css" />
  </head>
  <body>
    <h1>Find a Restaurant Here</h1>
 
    <form action="/search" method="GET">
      <input
        autofocus
        id="search"
        type="search"
        name="q"
        placeholder="Enter a search term..."
      />
      <input
        id="location"
        type="search"
        name="location"
        placeholder="Enter your city..."
      />
      <input type="submit" value="Submit" />
    </form>
    </body>
</html>

Save and run your code (as we did before with go run main.go). What happens when you submit something into the form?

Solution

The URL changed! It became locahost:8000/search?q=query&location=city where query and city are replaced with whatever you typed into the box. Notice where in the attributes of the form you typed q and location.

We also have put a placeholder for each input box so that users know what to type into each box. They are also both type search boxes.

What's happening is that this form submits a GET request to our own local server by changing the URL to have the parameters (q and location) that were inputted. This way, Golang will be able to see when the URL has changed and respond accordingly.

If you're wondering what the difference between GET and POST requests are, check out this link.

Now let's figure out how to actually get data.

The Yelp API in Golang

Creating a Yelp API Account

Create a Yelp API account here and click Sign up.

Once you have created an account, go here to "Create a New App".

Create a New App Yelp API

Fill in the App Name, pick an Industry (randomly) and fill out the contact email and put a small description. The information in this form is irrelevant to the API's function.

Next you should get a page like this:

App Dashboard Page

Your API key is basically the password to access the resources of the API and the Client ID is a way to identify your App.

You will need the API Key soon.

The SearchHandler

The way this will all come together is by creating a function that runs when the form on the page is filled out. Luckily our mux Http multiplexer makes it easy. We can call a function when our url has /search in it.

Try to have mux call searchHandler when the URL has /search in it. Think about how you did it for indexHandler.

Solution


func main() {
    fmt.Println("App Started")

    mux := http.NewServeMux() // helps to call the correct handler based on the URL

    fs := http.FileServer(http.Dir("assets")) // put the "static" files on the server
    mux.Handle("/assets/", http.StripPrefix("/assets/", fs))

    mux.HandleFunc("/", indexHandler)        // what function to call on main page
    mux.HandleFunc("/search", searchHandler) // function to call on search
    http.ListenAndServe(":3000", mux)        // start a localserver to run files
}

Here we are adding one line to call searchHandler when the url contains /search. You might be wondering, where's searchHandler. Let's make it

Start with this code for searchHandler:

func searchHandler(w http.ResponseWriter, r *http.Request) {
    u, err := url.Parse(r.URL.String()) // gets the string from the URL and splits it up
    if err != nil {                     // check for errors
        w.WriteHeader(http.StatusInternalServerError)
        w.Write([]byte("Internal server error"))
        return
    }

    params := u.Query()                                // get the parts at the end of the url
    searchKey := params.Get("q")                       // get the search Query
    location := params.Get("location")                 // get the inputted location
    searchKey = strings.ReplaceAll(searchKey, " ", "") // take out spaces
    location = strings.ReplaceAll(location, " ", "")

    fmt.Println("Search Query is: ", searchKey)
    fmt.Println("Location is: ", location)
}

We first start by "parsing" the URL string (analyzing and separating).

A request is created when we submit the form (and the URL changes). So, if there is an error, we should still give back a response. That is why we must call WriteHeader, to give it a response with an http.StatusInternalServerError.

Next, we get the parameters (params) from the end of our url, finding the query we typed in and the location we typed in. Then we get rid of all the spaces in our submission because the Yelp API doesn't like spaces. Finally, we print it out.

When you run it, what happens?

Solution You run and submit the form, and the screen goes blank. You get the values that you submitted, but why does it go blank? You will notice that in indexHandler you had to Execute a template for something to show up. Similarly, you must do the same in searchHandler for anything to show.

However, let's first get some data!

Get the Data

Here is how we are going to get the data:

client := &http.Client{}
req, err := http.NewRequest("GET", "https://api.yelp.com/v3/businesses/search?location="+location+"&term="+searchKey, nil) // create request for yelp api
if err != nil {
    fmt.Println(err)
}
req.Header.Set("Authorization", "Bearer <Put API Key Here>") // set authorization token
response, err := client.Do(req)                                                                                                                                            // do the request
if err != nil {
    fmt.Println(err) // print the error if there is one
}

fmt.Println(response)

Put this code right under location = strings.ReplaceAll(location, " ", "").

We first create an http.Client which allows us to make requests to the API. Then we start formatting the request. We are making what's called a GET request to this API because we are getting data from it without sending our own data. Using the documentation of the Yelp API, we can see that location and term are the two url parameters we need to add to our url that we are requesting from. We put in the location and searchKey that we grabbed from the URL earlier.

We also need to authorize our request by sending our 'password', also known as the API Key, to the server. We put this in the Header of our request.

Then we Do the request and check for errors.

What happens?

Solution

You should have a bunch of gibberish show up in your terminal / command line once you submit something to the form on your page. Believe it or not, that is exactly what we want. However, we need to parse this response to get what we want out of it.

So, let's parse it!

Parsing the Received Data

If you go to the Yelp Documentation and scroll down, you will see an example Response Body. That is what the response looks like once parsed and formatted. We want to take this and have it get parsed in Golang. Therefore, we are going to put this data into a Go struct.

Copy the example response data and put it into JSON-To-Go so that it generates a struct that will parse the received data. If you get an error, you're going to have to remove the // ... and the comma before it near the bottom of the copied response data. Like so:

{
  "total": 8228,
  "businesses": [
    {
      "rating": 4,
      "price": "$",
      "phone": "+14152520800",
      "id": "E8RJkjfdcwgtyoPMjQ_Olg",
      "alias": "four-barrel-coffee-san-francisco",
      "is_closed": false,
      "categories": [
        {
          "alias": "coffee",
          "title": "Coffee & Tea"
        }
      ],
      "review_count": 1738,
      "name": "Four Barrel Coffee",
      "url": "https://www.yelp.com/biz/four-barrel-coffee-san-francisco",
      "coordinates": {
        "latitude": 37.7670169511878,
        "longitude": -122.42184275
      },
      "image_url": "http://s3-media2.fl.yelpcdn.com/bphoto/MmgtASP3l_t4tPCL1iAsCg/o.jpg",
      "location": {
        "city": "San Francisco",
        "country": "US",
        "address2": "",
        "address3": "",
        "state": "CA",
        "address1": "375 Valencia St",
        "zip_code": "94103"
      },
      "distance": 1604.23,
      "transactions": ["pickup", "delivery"]
    }
  ],
  "region": {
    "center": {
      "latitude": 37.767413217936834,
      "longitude": -122.42820739746094
    }
  }
}

Awesome you should get something like this:

type AutoGenerated struct {
    Total      int `json:"total"`
    Businesses []struct {
        Rating     int    `json:"rating"`
        Price      string `json:"price"`
        Phone      string `json:"phone"`
        ID         string `json:"id"`
        Alias      string `json:"alias"`
        IsClosed   bool   `json:"is_closed"`
        Categories []struct {
            Alias string `json:"alias"`
            Title string `json:"title"`
        } `json:"categories"`
        ReviewCount int    `json:"review_count"`
        Name        string `json:"name"`
        URL         string `json:"url"`
        Coordinates struct {
            Latitude  float64 `json:"latitude"`
            Longitude float64 `json:"longitude"`
        } `json:"coordinates"`
        ImageURL string `json:"image_url"`
        Location struct {
            City     string `json:"city"`
            Country  string `json:"country"`
            Address2 string `json:"address2"`
            Address3 string `json:"address3"`
            State    string `json:"state"`
            Address1 string `json:"address1"`
            ZipCode  string `json:"zip_code"`
        } `json:"location"`
        Distance     float64  `json:"distance"`
        Transactions []string `json:"transactions"`
    } `json:"businesses"`
    Region struct {
        Center struct {
            Latitude  float64 `json:"latitude"`
            Longitude float64 `json:"longitude"`
        } `json:"center"`
    } `json:"region"`
}

But, this looks like a mess, so let's simplify a little bit.

type Business struct {
    Rating     int    `json:"rating"`
    Price      string `json:"price"`
    Phone      string `json:"phone"`
    ID         string `json:"id"`
    Alias      string `json:"alias"`
    IsClosed   bool   `json:"is_closed"`
    Categories []struct {
        Alias string `json:"alias"`
        Title string `json:"title"`
    } `json:"categories"`
    ReviewCount int    `json:"review_count"`
    Name        string `json:"name"`
    URL         string `json:"url"`
    Coordinates struct {
        Latitude  float64 `json:"latitude"`
        Longitude float64 `json:"longitude"`
    } `json:"coordinates"`
    ImageURL string `json:"image_url"`
    Location struct {
        City     string `json:"city"`
        Country  string `json:"country"`
        Address2 string `json:"address2"`
        Address3 string `json:"address3"`
        State    string `json:"state"`
        Address1 string `json:"address1"`
        ZipCode  string `json:"zip_code"`
    } `json:"location"`
    Distance     float64  `json:"distance"`
    Transactions []string `json:"transactions"`
}

type Results struct {
    Total      int        `json:"total"`
    Businesses []Business `json:"businesses"`
}

Put this at the very bottom of your main.go. We will use the Results struct to store all the data (which contains multiple Business structs).

Let's put the data into this struct!

At the top of main.go, right after the import statement, put this

var results Results

Then, searchHandler should look like this:

func searchHandler(w http.ResponseWriter, r *http.Request) {
    u, err := url.Parse(r.URL.String()) // gets the string from the URL and splits it up
    if err != nil {                     // check for errors
        w.WriteHeader(http.StatusInternalServerError)
        w.Write([]byte("Internal server error"))
        return
    }

    params := u.Query()                                // get the parts at the end of the url
    searchKey := params.Get("q")                       // get the search Query
    location := params.Get("location")                 // get the inputted location
    searchKey = strings.ReplaceAll(searchKey, " ", "") // take out spaces
    location = strings.ReplaceAll(location, " ", "")

    client := &http.Client{}
    req, err := http.NewRequest("GET", "https://api.yelp.com/v3/businesses/search?location="+location+"&term="+searchKey, nil) // create request for yelp api
    if err != nil {
        fmt.Println(err)
    }
    req.Header.Set("Authorization", "Bearer <API Key Goes Here>") // set authorization token
    response, err := client.Do(req)                                                                                                                                            // do the request
    if err != nil {
        fmt.Println(err) // print the error if there is one
    }

    err = json.NewDecoder(response.Body).Decode(&results) // parse the response
    fmt.Println(results.Businesses)                       // print the businesses

    fmt.Println("Search Query is: ", searchKey)
    fmt.Println("Location is: ", location)
}

The only two lines added are the NewDecoder and the print statement right after! Congrats on making it here!

Now we take the Body of the response we get from our request and Decode it to put it into our results struct that we defined at the top. If you are wondering about the &, check out this link. The Decode function takes a pointer to a struct as a parameter.

Then we print out the businesses in results. The . operator is how we can reference things inside structs and nested structs.

What happens?

Solution

When you submit the form, all of the businesses get printed to the terminal (doesn't look to pretty, but its certainly in the terminal).

Awesome! We are getting close!

Displaying the data

We are able to make use of the templates in our Go file to display a Go struct in html!

index.html:

<!DOCTYPE html>
<html>
  <head>
    <meta charset="UTF-8" />
    <meta name="viewport" content="width=device-width, initial-scale=1" />
    <title>Golang Restaurant Finder</title>
    <link rel="stylesheet" href="/assets/styles.css" />
  </head>
  <body>
    <h1>Find a Restaurant Here</h1>

    <form action="/search" method="GET">
      <input
        autofocus
        id="search"
        type="search"
        name="q"
        placeholder="Enter a search term..."
      />
      <input
        autofocus
        id="location"
        type="search"
        name="location"
        placeholder="Enter your city..."
      />
      <input type="submit" value="Submit" />
    </form>

    {{ range .Businesses }}
    <div class="restaurantCard">
      <img src="{{.ImageURL}}" />
      <a href="{{ .URL }}" target="_blank">
        <h3>{{ .Name }}</h3>
        <p>Review Count: {{ .ReviewCount }}</p>
        <p>Price: {{ .Price }}</p>
      </a>
    </div>
    {{ end }}

  </body>
</html>

Between the {{ and the }}, we are able to put Golang.

At the end of searchHandler put this line:

err = tpl.Execute(w, results)                         // run the template, passing in the results from API

Everything works! But why?

Solution

At the end of searchHandler in main.go we tell Go to execute our template and pass to it our struct that contains all the businesses.

Inside index.html, whatever we passed to it (results) becomes the .. So when we do {{ range .Businesses }}, we can loop through every Business in results.Businesses.

Now, on each loop, the . becomes the Business that is currently being looped through. So, we grab the different information from each business to make a card for the business and it displays on the screen!

Congratulations!

How to take this further?

  • See what other information you can put in the card to display!
  • Play around with the styles and share it in the discord
  • See if you can include another parameter to the yelp API to filter further i.e. "categories"
  • Try to implement autocomplete in the search boxes using the Yelp autocomplete endpoint

Share what you've made!

Comments (0)