Build a Real-time Voting App with Pusher, Node and Bootstrap

Share this article

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:

Harry Potter with Chart JS

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”.

Preview of Hello World in Browser

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.

Harry Potter Form

I’m also going to add a chart container that will chart real-time as the votes are received.

Harry Potter App Landing page after adding Logo

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:

Harry Potter with Chart JS

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):

Pusher in Console

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.

Frequently Asked Questions (FAQs) about Building a Real-Time Voting App

How can I ensure the security of my real-time voting app?

Security is a crucial aspect when building a real-time voting app. To ensure the security of your app, you can use secure channels provided by Pusher. These channels allow you to authenticate users before they can access the data. You can also use HTTPS for all communication to prevent data interception. Additionally, consider implementing rate limiting to prevent spamming and abuse of the voting system.

Can I use other languages besides Node.js to build a real-time voting app?

Yes, you can use other languages to build a real-time voting app. Pusher provides libraries for several languages including PHP, Ruby, .NET, Python, and Java. However, the tutorial provided in the article uses Node.js due to its efficiency in handling real-time data and its compatibility with Pusher.

How can I scale my real-time voting app to handle more users?

To scale your real-time voting app, you can use Pusher’s scalable infrastructure which can handle millions of concurrent connections. You can also optimize your code to handle more users by using efficient data structures and algorithms, and by minimizing the amount of data sent over the network.

Can I customize the look and feel of my real-time voting app?

Yes, you can customize the look and feel of your real-time voting app. The tutorial uses Bootstrap for styling, but you can use any CSS framework or write your own custom CSS. You can also use JavaScript libraries like jQuery or React to enhance the user interface.

How can I test my real-time voting app to ensure it works correctly?

You can test your real-time voting app by using automated testing tools like Mocha or Jest. These tools allow you to write tests for your server-side code and your client-side code. You can also manually test your app by creating multiple votes and checking if the results are updated in real-time.

How can I deploy my real-time voting app to a live server?

You can deploy your real-time voting app to a live server by using platforms like Heroku or AWS. These platforms provide easy-to-use interfaces for deploying Node.js apps. You will also need to configure your Pusher app to use the live server’s URL.

Can I integrate my real-time voting app with other services?

Yes, you can integrate your real-time voting app with other services. For example, you can integrate it with a database to store the votes, or with an authentication service to identify users. Pusher also provides integrations with other services like Slack or Zapier.

How can I handle errors in my real-time voting app?

You can handle errors in your real-time voting app by using error handling middleware in Express. This allows you to catch and handle any errors that occur during the execution of your code. You can also use the error events provided by Pusher to handle any errors related to the real-time functionality.

Can I use the real-time voting app for other types of polls or surveys?

Yes, you can use the real-time voting app for other types of polls or surveys. You can customize the code to handle different types of questions and options. You can also add features like multiple choice questions or rating scales.

How can I improve the performance of my real-time voting app?

You can improve the performance of your real-time voting app by optimizing your code and your server configuration. For example, you can use gzip compression to reduce the size of the data sent over the network. You can also use caching to reduce the load on your server.

Praveen KumarPraveen Kumar
View Author

Praveen is a software and web developer, cloud computing consultant, full-stack developer, UX architect, a CEO, and even … a cook. You can find him at praveen.science.

bootstrapnodePusherWebSockets
Share this article
Read Next
Get the freshest news and resources for developers, designers and digital creators in your inbox each week