Build a Real-time Voting App with Pusher, Node and Bootstrap
In this article, I’ll walk you through building a full-stack, real-time Harry Potter house voting web application.
Real-time apps usually use WebSockets, a relatively new type of transfer protocol, as opposed to HTTP, which is a single-way communication that happens only when the user requests it. WebSockets allow for persistent communication between the server and the user, and all those users connected with the application, as long as the connection is kept open.
A real-time web application is one where information is transmitted (almost) instantaneously between users and the server (and, by extension, between users and other users). This is in contrast with traditional web apps where the client has to ask for information from the server. — Quora
Our Harry Potter voting web app will show options (all the four houses) and a chart on the right side that updates itself when a connected user votes.
To give you a brief idea of look and feel, the final application is going to look like this:
Here’s a small preview of how the real-time application works:
To make our application real-time, we’re going to use Pusher and WebSockets. Pusher sits as a real-time layer between your servers and your clients. It maintains persistent connections to the clients — over a WebSocket if possible, and falling back to HTTP-based connectivity — so that, as soon as your servers have new data to push to the clients, they can do so instantly via Pusher.
Building our Application
Let’s create our fresh application using the command npm init
. You’ll be interactively asked a few questions on the details of your application. Here’s what I had:
praveen@praveen.science ➜ Harry-Potter-Pusher $ npm init
{
"name": "harry-potter-pusher",
"version": "1.0.0",
"description": "A real-time voting application using Harry Potter's house selection for my article for Pusher.",
"main": "index.js",
"scripts": {
"test": "echo \"Error: no test specified\" && exit 1"
},
"repository": {
"type": "git",
"url": "git+https://github.com/praveenscience/Harry-Potter-Pusher.git"
},
"keywords": [
"Harry_Potter",
"Pusher",
"Voting",
"Real_Time",
"Web_Application"
],
"author": "Praveen Kumar Purushothaman",
"license": "ISC",
"bugs": {
"url": "https://github.com/praveenscience/Harry-Potter-Pusher/issues"
},
"homepage": "https://github.com/praveenscience/Harry-Potter-Pusher#readme"
}
Is this OK? (yes)
So, I left most settings with default values. Now it’s time to install dependencies.
Installing Dependencies
We need Express, body-parser, Cross Origin Resource Sharing (CORS), Mongoose and Pusher installed as dependencies. To install everything in a single command, use the following. You can also have a glance of what this command outputs.
praveen@praveen.science ➜ Harry-Potter-Pusher $ npm i express body-parser cors pusher mongoose
npm notice created a lockfile as package-lock.json. You should commit this file.
npm WARN ajv-keywords@3.2.0 requires a peer of ajv@^6.0.0 but none is installed. You must install peer dependencies yourself.
+ pusher@2.1.2
+ body-parser@1.18.3
+ mongoose@5.2.6
+ cors@2.8.4
+ express@4.16.3
added 264 packages in 40.000s
Requiring Our Modules
Since this is an Express application, we need to include express()
as the first thing. While doing it, we also need some accompanying modules. So, initially, let’s start with this:
const express = require("express");
const path = require("path");
const bodyParser = require("body-parser");
const cors = require("cors");
Creating the Express App
Let’s start with building our Express application now. To start with, we need to get the returned object of the express()
function assigned to a new variable app
:
const app = express();
Serving Static Assets
Adding the above line after the initial set of includes will initialize our app
as an Express application. The next thing we need to do is to set up the static resources. Let’s create a new directory in our current project called public
and let’s use Express’s static middleware to serve the static files. Inside the directory, let’s create a simple index.html
file that says “Hello, World”:
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width" />
<title>Hello, World</title>
</head>
<body>
Hello, World!
</body>
</html>
To serve the static files, we have a built-in .use() function with express.static() in Express. The syntax is as follows:
app.use( express.static( path.join(__dirname, "public") ) );
We also need to use the body parser middleware for getting the HTTP POST content as JSON to access within the req.body
. We’ll also use urlencoded
to get the middleware that only parses urlencoded
bodies and only looks at requests where the Content-Type
header matches the type
option. This parser accepts only UTF-8 encoding of the body and supports automatic inflation of gzip
and deflate
encodings:
app.use( bodyParser.json() );
app.use( bodyParser.urlencoded( { extended: false } ) );
To allow cross-domain requests, we need to enable CORS. Let’s enable the CORS module by using the following code:
app.use( cors() );
Now all the initial configuration has been set. All we need to do now is to set a port and listen to the incoming connections on the specific port:
const port = 3000;
app.listen(port, () => {
console.log(`Server started on port ${port}.`);
});
Make sure your final app.js
looks like this:
const express = require("express");
const path = require("path");
const bodyParser = require("body-parser");
const cors = require("cors");
// Create an App.
const app = express();
// Serve the static files from public.
app.use( express.static( path.join(__dirname, "public") ) );
// Include the body-parser middleware.
app.use( bodyParser.json() );
app.use( bodyParser.urlencoded( { extended: false } ) );
// Enable CORS.
app.use( cors() );
// Set the port.
const port = 3000;
// Listen to incoming connections.
app.listen(port, () => {
console.log(`Server started on port ${port}.`);
});
Run the command to start the server:
$ npm run dev
Open your http://localhost:3000/
on a new tab and see the magic. You should be seeing a new page with “Hello, World”.
Building the App’s Back End
First thing, let’s create a directory called routes
and create a file in there, say vote.js
. We need to connect this file with our app.js
file, so let’s go back to it and include it under our express()
initialization:
const app = express();
// Vote route.
const vote = require("./routes/vote")
Since the routes
directory is in the same directory as the app.js
, we’ll start with ./
. To be able to use this one with the router, let’s go to the bottom and add the router middleware code shown below before the port definition:
app.use("/vote", vote);
Anything that goes through the /vote
URL is going to be handled by the vote.js
file, provided by the vote
variable.
Handling GET and POST Requests
Using Express’s Router instance, we can handle the GET
and POST
methods through our /vote
path. Let’s create a default GET
route now and send a default text say, "You are in /vote"
.
const express = require("express");
const router = express.Router();
// Default get route.
router.get("/", (req, res) => {
res.send("You are in /vote");
});
The code above routes all the requests to the path /vote
to our newly formed routes/vote.js
.
Handling POST Requests
We also need a POST
handler, where we can trigger the Pusher API. It’s going to be Router.post()
for the POST
requests to the /
so that all the requests will go to /vote
because of our middleware. We’ll be giving the same type of arrow functions here, and let’s give a message like "You have POSTed to /vote."
:
// Default POST route.
router.post("/", (req, res) => {
res.send("You have POSTed to /vote.");
});
The res.send()
function will be replaced by Pusher API calls in the future.
Exporting the Router
Finally, we have to export the router as a module. Use the module.exports
like this at the end. This should be the end of the file, although you can have it any where. Remember, JavaScript is event oriented and not procedural:
// Export the router.
module.exports = router;
At this point, when you see the full vote.js
file, it should look something like this:
const express = require("express");
const router = express.Router();
// Default GET route.
router.get("/", (req, res) => {
res.send("You are in /vote.");
});
// Default POST route.
router.post("/", (req, res) => {
res.send("You have POSTed to /vote.");
});
// Export the router.
module.exports = router;
Make sure you save everything and now try to run both the URLs in our web browser.
You should see output in your web browsers.
Integrating with the Pusher API
Let’s start by modifying the code we wrote for the POST
handler — the one we wrote in the vote.js
file. Here’s what we actually want to trigger Pusher. Let’s quickly go to our Pusher Dashboard, and select your Pusher App (praveen-science-app
, in my case) and click on the Getting Started tab. You’ll see the code to kickstart.
In our vote.js
we need to define (or require) the Pusher library. Then we need to create a new instance (an object) of the Pusher
class, and then finally trigger the pusher service inside the POST
. I’m changing the vote.js
file as shown below:
Note: make sure you change your appId
, key
, secret
to the one given in the dashboard.
const express = require("express");
const router = express.Router();
// ///// Step 1: Include Pusher ///// //
const Pusher = require('pusher');
// ///// Step 2: Instantiate an Object ///// //
const pusher = new Pusher({
appId: 'appId',
key: 'key',
secret: 'secret',
cluster: 'eu',
encrypted: true
});
// Default GET route.
router.get("/", (req, res) => {
res.send("You are in /vote.");
});
// Default POST route.
router.post("/", (req, res) => {
// ///// Step 3: Trigger the Pusher service ///// //
pusher.trigger('my-channel', 'my-event', {
"message": "hello world"
});
});
// Export the router.
module.exports = router;
The moment a user submits the form, our application fires a POST
request to this route, which is going to hit the Pusher API and trigger the Pusher service using the pusher.trigger()
function call. Also, we don’t want to use the default values of my-channel
and my-event
, so let’s change them to hp-voting
and hp-house
. We also don’t need to send a message
, but instead, we’d like to give points
and the house
information:
router.post("/", (req, res) => {
pusher.trigger('hp-voting', 'hp-house', {
"points": 1,
"house": req.body.house
});
});
For now, we’re going to assign a value of 1
to the points
(I’ll explain why shortly), and we’re using req.body.house
for the house
, because the value is going to come from the form data, and that is provided using req.body
as we’re using body-parser
.
Finally, we’ll return a JSON to the browser as response using the res.json()
function and pass an object with a boolean success
and a message
thanking the user for voting and that it has been received successfully:
router.post("/", (req, res) => {
pusher.trigger('hp-voting', 'hp-house', {
"points": 1,
"house": req.body.house
});
return res.json({
"success": true,
"message": "Thanks for voting."
});
});
Building the App’s Front End
I’ve used jQuery and Bootstrap to do the front end. Here’s the part where we allow the users to vote.
I’m also going to add a chart container that will chart real-time as the votes are received.
Integrating Everything
We already have the back end done. Now we’ll see how to send the request to the Pusher service when the vote button is clicked, thanks to the front-end JavaScript. We’ll trigger the submit
event of the form when the user clicks the button, and it needs to make a POST
request to our back end to the route /vote
.
Event Listener, User Data and Ajax
Let’s add an event listener for our form submission, code for capturing the user data, and the Ajax call:
// Execute only after the whole document is fetched and assets are loaded.
$(document).ready(function () {
// Form submission event listener (event handler)
$("#voteForm").submit(function (e) {
e.preventDefault();
// Get the checked input element's value.
var house = $(".form-check-input:checked").val();
// Construct the data to be sent as a payload to the AJAX call.
var data = {
"house": house
};
$.post("/vote", data, function (res) {
// Log the output in the console.
console.log(res);
});
});
});
Working with Pusher and Charts
When the form is submitted, the Ajax call triggers the /vote
endpoint, and the back-end Node app will also trigger the Pusher service using the code below in routes/vote.js
:
pusher.trigger('hp-voting', 'hp-house', {
"points": 1,
"house": req.body.house
});
When the code above is hit (or run), Pusher service will trigger an event with hp-voting
and hp-house
. We’re not catching the event or subscribing it yet. So we’re going to implement CanvasJS to build our chart, and we’re going to subscribe to the above event and will be adding the data points by the trigger, specified by the form’s submit
event listener.
Adding CanvasJS
Once you add all the bits correctly, your client-side script.js
should be similar to this:
// Execute only after the whole document is fetched and assets are loaded.
$(document).ready(function () {
// Form submission event listener (event handler)
$("#voteForm").submit(function (e) {
// Prevent the default event.
e.preventDefault();
// Get the checked input element's value.
var house = $(".form-check-input:checked").val();
// Construct the data to be sent as a payload to the Ajax call.
var data = {
"house": house
};
// Fire the POST request Ajax call to our /vote end point.
$.post("/vote", data, function (res) {
// Log the output in the console.
console.log(res);
});
});
// Create the base data points.
var dataPoints = [
{
label: "Gryffindor",
y: 0
}, {
label: "Hufflepuff",
y: 0
}, {
label: "Ravenclaw",
y: 0
}, {
label: "Slytherin",
y: 0
}
];
// Initialize Chart using jQuery selector.
// Get the chart container element.
var chartContainer = $("#chartContainer");
// Check if the element exists in the DOM.
if (chartContainer.length === 1) {
// Construct the options for the chart.
var options = {
"animationEnabled": true,
"theme": "light1",
"title": {
"text": "Harry Potter House Results"
},
"data": [
{
"type": "column",
"dataPoints": dataPoints
}
]
};
// Initialize the chart.
$("#chartContainer").CanvasJSChart(options);
}
});
Now save the file, and when you reload the page, you should be able to see a placeholder chart. It is definitely a real chart but without any values. You should be able to see something like this:
Now we have implemented our CanvasJS chart on the right side.
Initialization of Pusher on the Client Side
After the Pusher Logging, we have to initialize the Pusher
object. Since we already have a client-side config.js
, we’ll be leveraging that code in this part:
// Initialise a Pusher Object.
var pusher = new Pusher(PusherConfig.key, {
cluster: PusherConfig.cluster,
forceTLS: PusherConfigforceTLS.
});
After initialization of the Pusher object, we need to subscribe to our channel, where our messages get published by the server side. We’ll be copying the code from the Pusher Dashboard but changing a little bit to subscribe to our hp-voting
channel and hp-house
event. The default values of my-channel
and my-event
need to be updated like this in relation with our back-end code:
// Subscribe to the channel.
var channel = pusher.subscribe('hp-voting');
// Bind to a particular event and listen to the event data.
channel.bind('hp-house', function(data) {
alert(JSON.stringify(data));
});
Instead of alert
-ing a data
message, we want to basically add the data to the chart. The way we can do that is by taking our dataPoints
and manipulating the array with respect to the server’s response. With the already existing dataPoints
variable (remember that we’ve used var
instead of const
as we should be able to change it at a later stage), we’ll be using a higher-order Array.map() function as follows:
// Bind to a particular event and listen to the event data.
channel.bind('hp-house', function(data) {
// Use a higher order Array map.
dataPoints = dataPoints.map(function (d) {
// Check if the current label is the updated value.
if (d.label == data.house) {
// Increment the house's value by the number of new points.
d.y += data.points;
}
// Return the original value as this is a map function.
return d;
});
});
We’re looping through all the dataPoints
labels, and when a particular label matches the current label, we’ll be incrementing the value of the current label with the number of points updated. Since we’re using a JavaScript Array.map()
function, we have to return the original value, d
, back to the function call. Once we’ve updated the dataPoints
, we have to re-render the chart.
After the map()
function, we’ll do the following:
channel.bind('hp-house', function(data) {
// Use a higher order Array map.
dataPoints = dataPoints.map(function (d) {
// Check if the current label is the updated value.
if (d.label == data.house) {
// Increment the house's value by the number of new points.
d.y += data.points;
}
// Return the original value as this is a map function.
return d;
});
// Re-render the chart.
$("#chartContainer").CanvasJSChart(options);
});
Once you write all the above code, save and run the application in your browser, fire up your web developer tools and check out the console. You should see that Pusher Service is communicating with your application. I was able to see the following in my console (I’ve hidden my appId
and secret
, so other than those sensitive bits of information, everything else is shown):
Conclusion
At this point of time, when you try to open two windows of the same application and vote on one screen, you can see all the screens updating at the same time. This is basically how you use the Pusher service to create real-time applications.
Our Harry Potter voting web app now shows options (all the four houses) and a chart on the right side that updates itself when a connected user votes. The next obvious step would be to use a database, such as MongoDB, to store all the information to make sure it persists even when we reload the pages of the application.