Build a live poll using Node.js

Introduction

An electronic poll simplifies the way polls are carried out and aggregates data in realtime. (These days, nobody needs to take a bus to town just to cast a vote for their favorite soccer team!) As voters cast their votes, every connected client that is authorised to see the poll data should see the votes as they come in.

This article explains how to seamlessly add realtime features to your polling app using Pusher while visualising the data on a chart using CanvasJS, in just 5 steps.

Some of the tools we will be using to build our app are:

  • Node: JavaScript on a server. Node will handle all our server related needs.
  • Express: Node utility for handling HTTP requests via routes
  • Body Parser: Attaches the request payload on Express’s req, hence req.body stores this payload for each request.
  • Pusher Channels: Sign up for a free Pusher account.
  • CanvasJS: A UI library to facilitate data visualization with JavaScript on the DOM.

Together, we will build a minimalist app where users can select their favourite JavaScript framework. Our app will also include an admin page where the survey owner can see the polls come in.

Let's walk through the steps one by one:

Polling screen

First things first. The survey participants or voters (call them whatever fits your context) need to be served with a polling screen. This screen contains clickable items from which they are asked to pick an option.

live-poll-nodejs-screen

Try not to get personal with the options, we're just making a realtime demo. The following is the HTML behind the scenes:

1<!-- ./index.html -->
2<div class="main">
3      <div class="container">
4        <h1>Pick your favorite</h1>
5        <div class="col-md-8 col-md-offset-2">
6          <div class="row">
7            <div class="col-md-6">
8              <div class="poll-logo angular">
9                <img src="images/angular.svg" alt="">
10              </div>
11            </div>
12            <div class="col-md-6">
13              <div class="poll-logo ember">
14                <img src="images/ember.svg" alt="">
15              </div>
16            </div>
17          </div>
18          <div class="row">
19            <div class="col-md-6">
20              <div class="poll-logo react">
21                <img src="images/react.svg" alt="">
22              </div>
23            </div>
24            <div class="col-md-6">
25              <div class="poll-logo vue">
26                <img src="images/vue.svg" alt="">
27              </div>
28            </div>
29          </div>
30        </div>
31      </div>
32    </div>
33    <div class="js-logo">
34      <img src="images/js.png" alt="">
35    </div>
36    <script src="https://cdnjs.cloudflare.com/ajax/libs/axios/0.16.2/axios.js"></script>
37    <script src="app.js"></script>

The HTML renders the polling cards and imports axios and our custom app.js file. axios will be used to make HTTP calls to a server we will create. This server is responsible for triggering/emitting realtime events using Pusher.

Send vote requests

When a user clicks on their chosen option, we want to react with a response. The response would be to trigger a HTTP request. This request is expected to create a Pusher event, but we are yet to implement that:

1// ./app.js
2window.addEventListener('load', () => {
3  var app = {
4    pollLogo: document.querySelectorAll('.poll-logo'),
5    frameworks: ['Angular', 'Ember', 'React', 'Vue']
6  }
7 
8  // Sends a POST request to the
9  // server using axios
10  app.handlePollEvent = function(event, index) {
11    const framework = this.frameworks[index];
12    axios.post('http://localhost:3000/vote', {framework: framework})
13    .then((data) => {
14      alert (`Voted ${framework}`);
15    })
16  }
17
18  // Sets up click events for
19  // all the cards on the DOM
20  app.setup = function() {
21    this.pollLogo.forEach((pollBox, index) => {
22      pollBox.addEventListener('click', (event) => {
23        // Calls the event handler
24        this.handlePollEvent(event, index)
25      }, true)
26    })
27  }
28
29  app.setup();
30
31})

When each of the cards are clicked, handlePollEvent is called with the right values as argument depending on the index. The method, in turn, sends the framework name to the server as payload via the /vote (yet to be implemented) endpoint.

Set up a Pusher Channels

Before we jump right into setting up a server where Pusher will trigger events based on the request sent from the client, sign up for a free Pusher account. Then go to the dashboard and create a Channels app instance.

Configure your app by providing basic information requested in the form presented. You can also choose the environment you intend to integrate Pusher into for a better setup experience.

You can retrieve your app credentials from the App Keys tab

Realtime server

The easiest way to set up a Node server is by using the Express project generator. You need to install this generator globally on your machine using npm:

npm install express-generator -g

The generator is a scaffold tool, therefore it’s useless after installation unless we use its command to create a new Express app. We can do that by running the following command:

express poll-server

This generates a few helpful files including the important entry point (app.js) and routes (found in the routes folder).

We just need one route to get things moving: a /vote route which is where the client is sending a post request.

Create a new vote.js file in the routes folder with the following logic:

1// ./routes/votes.js
2var express = require('express');
3var Pusher = require('pusher');
4
5var router = express.Router();
6var pusher = new Pusher({
7  appId: '<APP_ID>',
8  key: '<APP_KEY>',
9  secret: '<APP_SECRET>',
10  cluster: '<APP_CLUSTER>',
11  encrypted: true
12});
13// /* Vote
14router.post('/', function(req, res, next) {
15  pusher.trigger('poll', 'vote', {
16    points: 10,
17    framework: req.body.framework
18  });
19  res.send('Voted');
20});
21module.exports = router;

For the above snippet to run successfully, we need to install the Pusher SDK using npm. The module is already used but it’s not installed yet:

npm install --save pusher
  • At the top of the file, we import Express and Pusher Channels, then configure a route with Express and a Channels instance with the credentials we retrieved from the Pusher dashboard.
  • The configured router is used to create a POST /vote route which, when hit, triggers a Pusher event. The trigger is achieved using the trigger method which takes the trigger identifier(poll), an event name (vote), and a payload.
  • The payload can be any value, but in this case we have a JS object. This object contains the points for each vote and the name of the option (in this case, a framework) being voted. The name of the framework is sent from the client and received by the server using req.body.framework .
  • We still go ahead to respond with “Voted” string so we don’t leave the server hanging in the middle of an incomplete request.

In the app.js file, we need to import the route we have just created and add it as part of our Express middleware. We also need to configure CORS because our client lives in a different domain, therefore the requests will NOT be made from the same domain:

// ./app.js
1// Other Imports
2var vote = require('./routes/vote');
3
4// CORS
5app.all('/*', function(req, res, next) {
6  // CORS headers
7  res.header("Access-Control-Allow-Origin", "*");
8  // Only allow POST requests
9  res.header('Access-Control-Allow-Methods', 'POST');
10  // Set custom headers for CORS
11  res.header('Access-Control-Allow-Headers', 'Content-type,Accept,X-Access-Token,X-Key');
12});
13
14// Ensure that the CORS configuration
15// above comes before the route middleware
16// below
17app.use('/vote', vote);
18
19module.exports = app;

Connect a dashboard

The last step is the most interesting aspect of the example. We will create another page in the browser which displays a chart of the votes for each framework. We intend to access this dashboard via the client domain but on the /admin.html route.

Here is the markup for the chart:

1<!-- ./admin.html -->
2<div class="main">
3  <div class="container">
4    <h1>Chart</h1>
5    <div id="chartContainer" style="height: 300px; width: 100%;"></div>
6  </div>
7</div>
8<script src="https://js.pusher.com/4.0/pusher.min.js"></script>
9<script src="https://cdnjs.cloudflare.com/ajax/libs/canvasjs/1.7.0/canvasjs.js"></script>
10<script src="app.js"></script>
  • The div with the id charContainer is where we will mount the chart.
  • We have imported Pusher Channels and Canvas JS (for the chart) via CDN as well as the same app.js that our home page uses.
live-poll-nodejs-chart

We need to initialize the chart with a default dataset. Because this is a simple example, we won’t bother with persisted data, rather we can just start at empty (zeros):

1// ./app.js
2window.addEventListener('load', () => {
3  // Event handlers for
4  // vote cards was here.
5  // Just truncated for brevity
6  
7  let dataPoints = [
8      { label: "Angular", y: 0 },
9      { label: "Ember", y: 0 },
10      { label: "React", y: 0 },
11      { label: "Vue", y: 0 },
12    ]
13    const chartContainer = document.querySelector('#chartContainer');
14    
15    if(chartContainer) {
16      var chart = new CanvasJS.Chart("chartContainer",
17        {
18          animationEnabled: true,
19          theme: "theme2",
20          data: [
21          {
22            type: "column",
23            dataPoints: dataPoints
24          }
25          ]
26        });
27      chart.render();
28    }
29    
30    // Here:
31    // - Configure Pusher
32    // - Subscribe to Pusher events
33    // - Update chart
34})
  • The dataPoints array is the data source for the chart. The objects in the array have a uniform structure of label which stores the frameworks and y which stores the points.
  • We check if the chartContainer exists before creating the chart because the index.html file doesn’t have a chartContainer.
  • We use the Chart constructor function to create a chart by passing the configuration for the chart which includes the data. The chart is rendered by calling render() on constructor function instance.

We can start listening to Pusher events in the comment placeholder at the end:

1// ./app.js
2// ...continued
3// Allow information to be
4// logged to console
5Pusher.logToConsole = true;
6
7// Configure Pusher instance
8var pusher = new Pusher('<APP_KEY>', {
9  cluster: '<APP_CLUSTER>',
10  encrypted: true
11});
12
13// Subscribe to poll trigger
14var channel = pusher.subscribe('poll');
15// Listen to vote event
16channel.bind('vote', function(data) {
17  dataPoints = dataPoints.map(x => {
18    if(x.label == data.framework) {
19      // VOTE
20      x.y += data.points;
21      return x
22    } else {
23      return x
24    }
25  });
26  
27  // Re-render chart
28  chart.render()
29});
  • First we ask Pusher to log every information about realtime transfers to the console. You can leave that out in production.
  • We then configure Pusher with our credentials by passing the app key and config object as arguments to the Pusher constructor function.
  • The name of our trigger is poll, so we subscribe to it and listen to its vote event. Hence, when the event is triggered, we update the dataPoints variable and re-render the chart with render()
live-poll-nodejs-demo

Conclusion

We didn’t spend time building a full app with identity and all, but you should now understand the model for building a fully fleshed poll system. We just made a simple realtime poll app with Pusher Channels showing how powerful it can be.