Digital polls are a great way for an online crowd to express their opinions towards a set of items on the list. In the past, to participate in voting, voters would have to physically be present at the elected place of vote so that they can cast their ballots. Such a drag right?
In this tutorial we will demonstrate how to build a realtime voting poll application. We will write the backend API for handling the HTTP requests and saving updates to the database (SQLite) in Go.
We will be using the Go framework, Echo, to keep boilerplate to a minimum. You can think of Echo to be to Go what Laravel is to PHP. If you have prior experience using web frameworks to create routes and handle HTTP requests, the code in this tutorial should look somewhat familiar.
For the frontend section of this project, we’ll use Vue.js. With its reactive properties, Vue.js will re-render the DOM whenever there is an update to the upvotes
or downvotes
of a vote member. We’ll also require a bit of jQuery to handle some functionality.
To make things work in realtime, we’ll integrate Pusher Channels into the application. Pusher makes it very easy to create realtime applications.
When we are done with our application, here’s what we will have:
To follow along with this article, you will need the following:
Once you have all the above requirements, we can proceed.
To get started create a new directory in our $GOPATH
and launching that directory with an IDE. We can do this by running the commands below:
1$ cd $GOPATH/src 2 $ mkdir gopoll 3 $ cd gopoll
The directory above will be our project directory. Next create our first .go
file where our main function will go, we will call it poll.go
.
Let’s import some useful Go packages that we’ll be using within our project. For a start, we have to fetch the Echo and SQLite packages from GitHub. Run the following commands to pull in the packages:
1$ go get github.com/labstack/echo 2 $ go get github.com/labstack/echo/middleware 3 $ go get github.com/mattn/go-sqlite3
⚠️ If you use Windows and you encounter the error ‘cc.exe: sorry, unimplemented: 64-bit mode not compiled in ‘, then you need a Windows gcc port, such as https://sourceforge.net/projects/mingw-w64/. Also see this GitHub issue.
Open the poll.go
file and paste in the following code:
1package main 2 3 import ( 4 // "database/sql" 5 "github.com/labstack/echo" 6 "github.com/labstack/echo/middleware" 7 // _ "github.com/mattn/go-sqlite3" 8 )
Above we also imported the database/sql
library but we don’t have to use go get
because this is a part of the standard Go library.
To enable Go to run our application, we need a main
function, so lets create that before we think of creating the routes and setting up the database.
Open the poll.go
file and in there add the following code to the file:
1func main() { 2 e := echo.New() 3 4 // Middleware 5 e.Use(middleware.Logger()) 6 e.Use(middleware.Recover()) 7 8 // Define the HTTP routes 9 e.GET("/polls", func(c echo.Context) error { 10 return c.JSON(200, "GET Polls") 11 }) 12 13 e.PUT("/polls", func(c echo.Context) error { 14 return c.JSON(200, "PUT Polls") 15 }) 16 17 e.PUT("/polls/:id", func(c echo.Context) error { 18 return c.JSON(200, "UPDATE Poll " + c.Param("id")) 19 }) 20 21 // Start server 22 e.Logger.Fatal(e.Start(":9000")) 23 }
Awesome, we’ve created some basic routes and even if they don’t do more than echo ‘static’ text, they should be able to handle matching URL requests.
We included the final line because we want to instruct Go to start the application using Echo’s Start
method. This will start Go’s standard HTTP server and listen for requests on the port 9000
.
We can test the routes in our application as it is now by compiling it down, running it and making requests to the port 9000
of our local host with Postman.
$ go run poll.go
Now we can head over to Postman and point the address to localhost:9000/polls
with a GET
HTTP verb. To try the PUT request, we can use an address such as localhost:9000/polls/3
.
Assuming that everything works as we planned, you should get the following screens:
In the poll.go
file, we will write some code to initialize a database with a filename of Storage.db
on application run. The Sql
driver can create this file for us if it doesn’t already exist. After the database has been created, we will run a function to migrate and seed the database for us if it hasn’t already been migrated and seeded.
Open the poll.go
file and add the following functions to the file:
1func initDB(filepath string) *sql.DB { 2 db, err := sql.Open("sqlite3", filepath) 3 4 if err != nil { 5 panic(err) 6 } 7 8 if db == nil { 9 panic("db nil") 10 } 11 12 return db 13 } 14 15 func migrate(db *sql.DB) { 16 sql := ` 17 CREATE TABLE IF NOT EXISTS polls( 18 id INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT, 19 name VARCHAR NOT NULL, 20 topic VARCHAR NOT NULL, 21 src VARCHAR NOT NULL, 22 upvotes INTEGER NOT NULL, 23 downvotes INTEGER NOT NULL, 24 UNIQUE(name) 25 ); 26 27 INSERT OR IGNORE INTO polls(name, topic, src, upvotes, downvotes) VALUES('Angular','Awesome Angular', 'https://cdn.colorlib.com/wp/wp-content/uploads/sites/2/angular-logo.png', 1, 0); 28 29 INSERT OR IGNORE INTO polls(name, topic, src, upvotes, downvotes) VALUES('Vue', 'Voguish Vue','https://upload.wikimedia.org/wikipedia/commons/thumb/5/53/Vue.js_Logo.svg/400px-Vue.js_Logo.svg.png', 1, 0); 30 31 INSERT OR IGNORE INTO polls(name, topic, src, upvotes, downvotes) VALUES('React','Remarkable React','https://upload.wikimedia.org/wikipedia/commons/thumb/a/a7/React-icon.svg/1200px-React-icon.svg.png', 1, 0); 32 33 INSERT OR IGNORE INTO polls(name, topic, src, upvotes, downvotes) VALUES('Ember','Excellent Ember','https://cdn-images-1.medium.com/max/741/1*9oD6P0dEfPYp3Vkk2UTzCg.png', 1, 0); 34 35 INSERT OR IGNORE INTO polls(name, topic, src, upvotes, downvotes) VALUES('Knockout','Knightly Knockout','https://images.g2crowd.com/uploads/product/image/social_landscape/social_landscape_1489710848/knockout-js.png', 1, 0); 36 ` 37 _, err := db.Exec(sql) 38 39 if err != nil { 40 panic(err) 41 } 42 }
The first function, initDB
is pretty straightforward with its task, it makes an attempt to open a database file, or creates it when it doesn’t exist. In a case where it is unable to read the database file or create it, the program exits because the database is crucial to the logic of the application.
The migrate
function, does exactly what its name suggests. It runs an SQL statement against the database to ensure that the polls
table is created if it isn’t already created, and seeded with some initial values for this example.
For our example, we will be seeding the database with some values for a few JavaScript frameworks. Each framework will have a column for registering the state of upvotes
and downvotes
. Like the initDB
function, if the migrate
function fails to migrate and seed the database, the program will return an error.
Next open the poll.go
file and add the following into the main
function right after the middleware definitions:
1// [...] 2 3 // Initialize the database 4 db := initDB("storage.db") 5 migrate(db) 6 7 // [...]
Next, uncomment the imports in the poll.go
file. Now we can test to see if our application works. Run the following command to build and run the application:
$ go run poll.go
If we look at the project directory, there should be a storage.db
file there. This means that our code executed correctly.
Great, now let’s create the handlers.
We’ve already created the endpoints with which the frontend can interact with the backend. Now we need to build the backend logic that will handle the received requests on specific routes. We can achieve this by registering several handler functions of our own.
Let’s begin by creating and navigating into a new directory called handlers
:
1$ mkdir handlers 2 $ cd handlers
Let’s create a new handlers.go
file in this handlers
directory and paste the following code into the file:
1package handlers 2 3 import ( 4 "database/sql" 5 "net/http" 6 "strconv" 7 "github.com/labstack/echo" 8 )
Next, open the poll.go
file and import the handlers.go
package in there:
1import ( 2 // [...] 3 4 "gopoll/handlers" 5 6 // [...] 7 )
In the same file, replace the route definitions from earlier with the ones below:
1// [...] 2 3 // Define the HTTP routes 4 e.File("/", "public/index.html") 5 e.GET("/polls", handlers.GetPolls(db)) 6 e.PUT("/poll/:index", handlers.UpdatePoll(db)) 7 8 // [...]
You may have noticed that we included an extra route above:
e.File("/", "public/index.html")
This is the route that will process requests sent to the /
endpoint. We need this route to serve a static HTML
file that we are yet to create, this file will hold our client-side code and live in the public directory.
Now back to the handlers.go
file. In order for us to return arbitrary JSON as responses in our handler, we need to register a map just below our import statements:
type H map[string]interface{}
This maps strings as keys and anything else as values. In Go, the "interface" keyword represents anything from a primitive datatype to a user defined type or struct.
Let’s create our handlers. We will make it so they receive an instance of the database we’ll be passing from the routes. They’ll also need to implement the Echo.HandlerFunc interface so they can be used by the routes.
Open the handlers.go
file and paste the following:
1func GetPolls(db *sql.DB) echo.HandlerFunc { 2 return func(c echo.Context) error { 3 return c.JSON(http.StatusOK, models.GetPolls(db)) 4 } 5 } 6 7 func UpdatePoll(db *sql.DB) echo.HandlerFunc { 8 return func(c echo.Context) error { 9 var poll models.Poll 10 11 c.Bind(&poll) 12 13 index, _ := strconv.Atoi(c.Param("index")) 14 15 id, err := models.UpdatePoll(db, index, poll.Name, poll.Upvotes, poll.Downvotes) 16 17 if err == nil { 18 return c.JSON(http.StatusCreated, H{ 19 "affected": id, 20 }) 21 } 22 23 return err 24 } 25 }
The GetPolls
function returns the StatusOK
status code and passes the received instance of the database to a model function that we will create soon. In the next section, we’ll create the models package, define its functions and import it into the handlers package.
The UpdatePoll
function is defined to work on a single poll, it calls c.Bind
on an instance of models.Poll
; this call is responsible for taking a JSON
formatted body sent in a PUT
request and mapping it to a Poll struct. The Poll struct will be defined in the models package.
Since this handler will be receiving an index
parameter from the route, we are using the strconv
package and the Atoi
(alpha to integer) function to make sure the index is cast to an integer. This will ensure that we can correctly point to a row when we query the database. We have also done a bit of error checking in this function, we want to ensure that the application terminates properly if there is ever an error.
Let’s move on to the creation of the models package.
It is a good practice to keep codebases as modular as possible so we have avoided making direct calls to the database in the handlers
package. Instead, we will abstract the database logic into the models package so that the interactions are performed by the models.
Let’s create a new directory in the working directory of our application. This is where the models package will go, we can run this command:
$ mkdir models
In the models
directory create a new models.go
file and paste the following into the code:
1package models 2 3 import ( 4 "database/sql" 5 _ "github.com/mattn/go-sqlite3" 6 )
Next import the models package into the handlers.go
file:
1package handlers 2 3 import ( 4 // [...] 5 6 "gopoll/models" 7 8 // [...] 9 )
In the models package, let’s create a Poll type
which is a struct with six fields:
ID
- the id of the poll.Name
- the name of the poll.Topic
- the topic of the poll.Src
- the link to an image for the poll.Upvotes
- the number of upvotes on the poll.Downvotes
- the number of downvotes on the poll.In Go, we can add metadata to variables by putting them within backticks. We can use this feature to define what each field should look like when converted to JSON
. This will also help the c.Bind
function in the handlers.go
file to know how to map JSON
data when registering a new Poll.
We will also use the type
keyword to define a collection of Polls, this is required for when there is a request to return all the Polls in the database. We’d simply aggregate them into an instance of this collection and return them.
1type Poll struct { 2 ID int `json:"id"` 3 Name string `json:"name"` 4 Topic string `json:"topic"` 5 Src string `json:"src"` 6 Upvotes int `json:"upvotes"` 7 Downvotes int `json:"downvotes"` 8 } 9 10 type PollCollection struct { 11 Polls []Poll `json:"items"` 12 }
Now let’s define the GetPolls
function. This function will be responsible for getting the polls from the database, returning them as an instance of a Poll collection and returning them to the function that invoked it. This function doesn’t use any new features and is pretty straight forward:
1func GetPolls(db *sql.DB) PollCollection { 2 sql := "SELECT * FROM polls" 3 4 rows, err := db.Query(sql) 5 6 if err != nil { 7 panic(err) 8 } 9 10 defer rows.Close() 11 12 result := PollCollection{} 13 14 for rows.Next() { 15 poll := Poll{} 16 17 err2 := rows.Scan(&poll.ID, &poll.Name, &poll.Topic, &poll.Src, &poll.Upvotes, &poll.Downvotes) 18 19 if err2 != nil { 20 panic(err2) 21 } 22 23 result.Polls = append(result.Polls, poll) 24 } 25 26 return result 27 }
We also need to define an UpdatePoll
method that will update the state of the upvotes
and downvotes
of a Poll. In the same file paste the following code:
1func UpdatePoll(db *sql.DB, index int, name string, upvotes int, downvotes int) (int64, error) { 2 sql := "UPDATE polls SET (upvotes, downvotes) = (?, ?) WHERE id = ?" 3 4 // Create a prepared SQL statement 5 stmt, err := db.Prepare(sql) 6 7 // Exit if we get an error 8 if err != nil { 9 panic(err) 10 } 11 12 // Make sure to cleanup after the program exits 13 defer stmt.Close() 14 15 // Replace the '?' in our prepared statement with 'upvotes, downvotes, index' 16 result, err2 := stmt.Exec(upvotes, downvotes, index) 17 18 // Exit if we get an error 19 if err2 != nil { 20 panic(err2) 21 } 22 23 return result.RowsAffected() 24 }
You might have noticed we are using prepared SQL statements in the UpdatePoll
function. There are several benefits to doing this. We ensure SQL statements are always cleaned up and safe from SQL injection attacks. Prepared SQL statements also help our program execute faster since the statements will be compiled and cached for multiple uses.
Now that we are done with the backend, lets add some frontend code. Create a public
directory in the root directory of your project. In this directory create an index.html
file. This is where we will add most of the frontend magic.
Because we want to keep things simple, we will include the Vue.js and jQuery code in the index.html
file. Open the file and paste the following HTML code into it:
1<!DOCTYPE html> 2 <html lang="en"> 3 <head> 4 <meta charset="UTF-8"> 5 <meta name="viewport" content="width=device-width, initial-scale=1"> 6 <meta http-equiv="X-UA-Compatible" content="ie=edge"> 7 <link rel="stylesheet" href="https://maxcdn.bootstrapcdn.com/bootstrap/4.0.0-beta.2/css/bootstrap.min.css"> 8 <link rel="stylesheet" href="https://use.fontawesome.com/releases/v5.0.13/css/all.css"> 9 <script src="https://unpkg.com/axios/dist/axios.min.js"></script> 10 <script src="https://cdn.jsdelivr.net/npm/vue@2.5.16/dist/vue.js"></script> 11 <script src="https://cdnjs.cloudflare.com/ajax/libs/jquery/2.2.4/jquery.min.js"></script> 12 <title>A GO Voting Poll Application With Pusher </title> 13 </head> 14 <body> 15 <div id="msg" style="display: none; padding: 1em; position: fixed; margin: 0px 5px;"></div> 16 <div id="app" class="container"> 17 <div class="row" style="margin: 1em 0em" v-for="(poll, index) in polls"> 18 <div class="card col-md-4" style="margin: 20px auto; width: 25rem; background: rgb(93, 95, 104)"> 19 <img class="card-img-top" :src="poll.src" alt="Card image"> 20 <div class="card-body" > 21 <p class="card-text text-center" style="font-size: 1.5em; color: white; font-weight: bold"> {{ poll.topic }} as the best JS framework </p> 22 <form> 23 <div style="background: white; color: black; padding: 1em; border-radius: 5px;"> <input type="radio" :value="poll.name" :name="poll.name" @change="upvote(index)"> Yes <span style="padding-left: 60%;"><i class="fas fa-thumbs-up"></i> ({{ poll.upvotes }}) </span></div> 24 <hr> 25 <div style="background: white; color: black; padding: 1em; border-radius: 5px;"> <input type="radio" :value="poll.name" :name="poll.name" @change="downvote(index)" > No <span style="padding-left: 60%;"><i class="fas fa-thumbs-down"></i> ({{ poll.downvotes }}) </span></div> 26 </form> 27 <button class="btn btn-block" style="margin: 1em 0; background: #1bff8b; cursor: pointer; font-weight: bold" v-on:click="UpdatePoll(index)"> Vote </button> 28 </div> 29 </div> 30 </div> 31 </div> 32 </body> 33 </html>
Next in the same file, paste the following code before the closing body
tag of the HTML:
1<script> 2 var app = new Vue({ 3 el: '#app', 4 data: { 5 polls: [], 6 click: [], 7 }, 8 created: function () { 9 axios.get('/polls') 10 .then(res => this.polls = res.data.items ? res.data.items : []) 11 .catch(e => this.failed('Unsuccesful')) 12 }, 13 methods: { 14 upvote: function (n) { 15 if (this.click[n] == true) { 16 this.polls[n].downvotes -= 1; 17 this.polls[n].upvotes += 1; 18 } else { 19 this.polls[n].upvotes += 1; 20 this.click[n] = true; 21 } 22 }, 23 downvote: function (n) { 24 if (this.click[n] == true) { 25 this.polls[n].upvotes -= 1; 26 this.polls[n].downvotes += 1; 27 } else { 28 this.polls[n].downvotes += 1; 29 this.click[n] = true; 30 } 31 }, 32 UpdatePoll: function (index) { 33 let targetPoll = index + 1; 34 axios.put('/poll/' + targetPoll, this.polls[index]) 35 .then(res => this.approved('Successful')) 36 .catch(e => this.failed('Unsuccesful')) 37 }, 38 approved: function (data) { 39 $("#msg").css({ 40 "background-color": "rgb(94, 248, 94)", 41 "border-radius": "20px" 42 }); 43 $('#msg').html(data).fadeIn('slow'); 44 $('#msg').delay(3000).fadeOut('slow'); 45 }, 46 failed: function (data) { 47 $("#msg").css({ "background-color": "rgb(248, 66, 66)", "border-radius": "20px" }); 48 $('#msg').html(data).fadeIn('slow'); 49 $('#msg').delay(3000).fadeOut('slow'); 50 } 51 } 52 }) 53 </script>
Above we have our Vue code. We added the created()
life cycle hook so that Axios can make a GET
request to the backend API.
We’ve also defined two functions to keep track of the clicks on upvotes
or downvotes
to any members of the poll. These functions call another function, UpdatePoll
, which takes the index of the affected poll member as argument and makes a PUT request to the backend API for an update.
Lastly, we used jQuery to display matching divs
depending on if the update request was successful or unsuccessful.
Here’s a display of the application at the current level:
Next, head over to Pusher, you can create a free account if you don’t already have one. On the dashboard, create a new Channels app and copy out the app credentials (App ID, Key, Secret, and Cluster). We will use these credentials shortly.
To make sure our application is realtime, our backend must trigger an event when the poll is voted on.
To do this let’s pull in the Pusher Go library, which we will use to trigger events. Run the command below to pull in the package:
$ go get github.com/pusher/pusher-http-go
In the models.go
file, let’s import the Pusher Go library:
1package models 2 3 import ( 4 // [...] 5 6 pusher "github.com/pusher/pusher-http-go" 7 )
Then initialize the Pusher client. In the same file before the type definitions paste in the following:
1// [...] 2 3 var client = pusher.Client{ 4 AppId: "PUSHER_APP_ID", 5 Key: "PUSHER_APP_KEY", 6 Secret: "PUSHER_APP_SECRET", 7 Cluster: "PUSHER_APP_CLUSTER", 8 Secure: true, 9 } 10 11 // [...]
Here, we have initialized the Pusher client using the credentials from our earlier created app.
⚠️ Replace
PUSHER_*
keys with your app credentials.
Next, we will use our Pusher client to trigger an event, which will include the updates on the specific row in the database to be displayed as an update to the votes in our view. We will do this in the UpdatePoll
method, which updates the state of upvotes
and downvotes
in the database.
Replace the UpdatePoll
function with the following code:
1func UpdatePoll(db *sql.DB, index int, name string, upvotes int, downvotes int) (int64, error) { 2 sql := "UPDATE polls SET (upvotes, downvotes) = (?, ?) WHERE id = ?" 3 4 stmt, err := db.Prepare(sql) 5 6 if err != nil { 7 panic(err) 8 } 9 10 defer stmt.Close() 11 12 result, err2 := stmt.Exec(upvotes, downvotes, index) 13 14 if err2 != nil { 15 panic(err2) 16 } 17 18 pollUpdate := Poll{ 19 ID: index, 20 Name: name, 21 Upvotes: upvotes, 22 Downvotes: downvotes, 23 } 24 25 client.Trigger("poll-channel", "poll-update", pollUpdate) 26 return result.RowsAffected() 27 }
Above, we create a pollUpdate
object that holds the data for the most recent update to a row in the polls
table. This pollUpdate
object has all the data required for a realtime update on the client-side of our application, so will be passed to Pusher for transmission.
To display the realtime updates on votes, we will use the Pusher JavaScript client. Open your index.html
file and include the Pusher JavaScript library inside the head
tag like this:
<script src="https://js.pusher.com/4.1/pusher.min.js"></script>
Next, we want to go to the created()
method and create a Pusher instance using our app’s credentials:
1created: function() { 2 const pusher = new Pusher('PUSHER_APP_KEY', { 3 cluster: 'PUSHER_APP_CLUSTER', 4 encrypted: true 5 }); 6 7 // [...] 8 }
⚠️ Replace
PUSHER_APP_*
with values from your applications credentials.
Next, let’s subscribe to the poll-channel
and listen for the poll-update
event, where our votes updates will be transmitted. Right after the code we added above, paste the following:
1const channel = pusher.subscribe('poll-channel'); 2 3 channel.bind('poll-update', data => { 4 this.polls[data.id - 1].upvotes = data.upvotes; 5 this.polls[data.id - 1].downvotes = data.downvotes; 6 });
Note: We are subtracting from the
polls
array index because we need it to match the data received from Pusher. JavaScript arrays begin their index at 0, while SQL id starts at 1.
Now we can build our application and see that the realtime functionality in action.
$ go run poll.go
Once the application is running, we can point our browser to this address http://localhost:9000
In this article, we were able to trigger realtime updates on new votes and demonstrate how Pusher Channels works with Go applications. We also learnt, on an unrelated note, how to consume API’s using Vue.js.
The source code to the application is available on GitHub.