In this article, I’ll walk you through implementing your own realtime post statistics (we’ll limit ourselves to Likes) in a simple Node.js app.
In this article, I’ll walk you through implementing your own realtime post statistics (we’ll limit ourselves to Likes) in a simple Node.js app. Here’s how the app will work when done
On the home page of our app, users will see all posts and they can click a button to Like or Unlike a post. Whenever a user likes or unlikes a post, the likes count displayed next to the post should increment or decrement in every other browser tab or window where the page is open.
You can check out the source code of the completed application on Github.
This tutorial assumes you have Node.js and MongoDB installed. We’ll be using Express, a popular lightweight Node.js framework. Let’s get our app set up quickly by using the express application generator:
1# if you don't already have it installed 2 npm install express-generator -g 3 4 # create a new express app with view engine set to Handlebars (hbs) 5 express --view=hbs poster 6 cd poster && npm install 7[ ]
Then we’ll add our dependencies:
1npm install --save dotenv faker mongoose pusher
Here’s a breakdown of what each module is for.
.env
file.First, let’s define our data structures. We’ll limit the scope of this demo to two entities: users and posts. For users. we’ll store only their names. For posts, we’ll store:
Since the only detail, we need about our users is their names, we won’t set up a User model; we’ll reference the user’s name directly from our Post model. So, let’s create a file, models/post.js
:
1let mongoose = require('mongoose'); 2 3 let Post = mongoose.model('Post', { 4 text: String, 5 posted_at: Date, 6 likes_count: Number, 7 author: String 8 }); 9 10 module.exports = Post;
Now, we’ll write a small script to get some fake data into our database. Create a file called seed.js
in the bin
directory, with the following contents:
1#!/usr/bin/env node 2 3 let faker = require('faker'); 4 let Post = require('../models/post'); 5 6 // connect to MongoDB 7 require('mongoose').connect('mongodb://localhost/poster'); 8 9 // remove all data from the collection first 10 Post.remove({}) 11 .then(() => { 12 let posts = []; 13 for (let i = 0; i < 30; i++) { 14 posts.push({ 15 text: faker.lorem.sentence(), 16 posted_at: faker.date.past(), 17 likes_count: Math.round(Math.random() * 20), 18 author: faker.name.findName() 19 }); 20 } 21 return Post.create(posts); 22 }) 23 .then(() => { 24 process.exit(); 25 }) 26 .catch((e) => { 27 console.log(e); 28 process.exit(1); 29 });
Run the seed using node
(remember to start your MongoDB server by running sudo mongod
first):
1node bin/seed.js
Let’s set up the route and view for our home page.
The first thing we’ll do is add our MongoDB connection setup to our app.js
, so the connection gets created when our app gets booted.
1// below this line: 2 var app = express(); 3 4 // add this 5 require('mongoose').connect('mongodb://localhost/poster');
Next up, the route where we retrieve all posts from the db and pass them to the view. Replace the code in routes/index.js
with this:
1let router = require('express').Router(); 2 3 let Post = require('./../models/post'); 4 5 router.get('/', (req, res, next) => { 6 Post.find().exec((err, posts) => { 7 res.render('index', { posts: posts }); 8 }); 9 10 }); 11 12 module.exports = router;
Lastly, the view where we render the posts. We’ll use Bootstrap for some quick styling.
1<!DOCTYPE html> 2 <html> 3 <head> 4 <link rel="stylesheet" href="https://maxcdn.bootstrapcdn.com/bootstrap/3.3.7/css/bootstrap.min.css"/> 5 </head> 6 7 <body> 8 9 <div class="container-fluid text-center"> 10 11 {{#each posts }} 12 <div class="jumbotron"> 13 <div>by 14 <b>{{ this.author.name }}</b> 15 on 16 <small>{{ this.posted_at }}</small> 17 </div> 18 19 <div> 20 <p>{{ this.text }}</p> 21 </div> 22 23 <div class="row"> 24 <button onclick="actOnPost(event);" 25 data-post-id="{{ this.id }}">Like 26 </button> 27 <span id="likes-count-{{ this.id }}">{{ this.likes_count }}</span> 28 </div> 29 </div> 30 {{/each}} 31 32 </div> 33 34 </body> 35 </html>
A few notes:
likes_count
****field an id
which includes the post ID, so we can directly reference the correct likes_count
with just the post ID.**actOnPost**
) . This is where we’ll toggle the button text (Like → Unlike) and increment the likes_count
. (And the reverse for when it’s an Unlike button). We’ll implement that in a bit.When a user clicks on ‘Like’, here’s what we want to happen:
likes_count
in the database by 1.For unliking:
likes_count
in the database by 1.We’ll classify both Likes and Unlikes as actions that can be carried out on a post so we can handle them together.
Let’s add some JavaScript to our home page for the actOnPost
method. We’ll pull in Axios for easy HTTP requests.
1<!-- in index.hbs --> 2 <script src="https://unpkg.com/axios/dist/axios.min.js"></script> 3 <script> 4 var updatePostStats = { 5 Like: function (postId) { 6 document.querySelector('#likes-count-' + postId).textContent++; 7 }, 8 Unlike: function(postId) { 9 document.querySelector('#likes-count-' + postId).textContent--; 10 } 11 }; 12 13 var toggleButtonText = { 14 Like: function(button) { 15 button.textContent = "Unlike"; 16 }, 17 Unlike: function(button) { 18 button.textContent = "Like"; 19 } 20 }; 21 22 var actOnPost = function (event) { 23 var postId = event.target.dataset.postId; 24 var action = event.target.textContent.trim(); 25 toggleButtonText[action](event.target); 26 updatePostStats[action](postId); 27 axios.post('/posts/' + postId + '/act', { action: action }); 28 }; 29 </script>
Then we define the act route. We’ll add it in our routes/index.js
:
1router.post('/posts/:id/act', (req, res, next) => { 2 const action = req.body.action; 3 const counter = action === 'Like' ? 1 : -1; 4 Post.update({_id: req.params.id}, {$inc: {likes_count: counter}}, {}, (err, numberAffected) => { 5 res.send(''); 6 }); 7 });
Here, we change the likes_count
using MongoDB’s built-in $inc
operator for update operations.
At this point, we’ve got our regular Liking and Unliking feature in place. Now it’s time to notify other clients when such an action happens.
Let’s get our Pusher integration set up. Create a free Pusher account if you don’t have one already. Then visit your dashboard and create a new app and take note of your app’s credentials. Since we’re using the dotenv
package, we can put our Pusher credentials in a .env
file in the root directory of our project:
1PUSHER_APP_ID=WWWWWWWWW 2 PUSHER_APP_KEY=XXXXXXXXX 3 PUSHER_APP_SECRET=YYYYYYYY 4 PUSHER_APP_CLUSTER=ZZZZZZZZ
Replace the stubs above with your app credentials from your Pusher dashboard. Then add the following line to the top of your app.js
:
1require('dotenv').config();
Next, we’ll modify our route handler to trigger a Pusher message whenever an action updates the likes_count
in the database. We’ll initialize an instance of the Pusher client and use it to send a message by calling pusher.trigger
.
The trigger
method takes four parameters:
Here’s what we want our payload to look like in the case of a Like
action:
1{ 2 "action": "Like", 3 "postId": 1234 4 }
So let’s add this logic to our route handler:
1let Pusher = require('pusher'); 2 let pusher = new Pusher({ 3 appId: process.env.PUSHER_APP_ID, 4 key: process.env.PUSHER_APP_KEY, 5 secret: process.env.PUSHER_APP_SECRET, 6 cluster: process.env.PUSHER_APP_CLUSTER 7 }); 8 9 router.post('/posts/:id/act', (req, res, next) => { 10 const action = req.body.action; 11 const counter = action === 'Like' ? 1 : -1; 12 Post.update({_id: req.params.id}, {$inc: {likes_count: counter}}, {}, (err, numberAffected) => { 13 pusher.trigger('post-events', 'postAction', { action: action, postId: req.params.id }, req.body.socketId); 14 res.send(''); 15 }); 16 });
On the client side (index.hbs
) we need to handle two things:
post-events
channelact
API request, so the server can use it to exclude the clientWe’ll pull in the Pusher SDK
1<script src="https://js.pusher.com/4.1/pusher.min.js"></script> 2 3 <script> 4 var pusher = new Pusher('your-app-id', { 5 cluster: 'your-app-cluster' 6 }); 7 var socketId; 8 9 // retrieve the socket ID on successful connection 10 pusher.connection.bind('connected', function() { 11 socketId = pusher.connection.socket_id; 12 }); 13 14 15 var channel = pusher.subscribe('post-events'); 16 channel.bind('postAction', function(data) { 17 // log message data to console - for debugging purposes 18 console.log(data); 19 var action = data.action; 20 updatePostStats[action](data.postId); 21 }); 22 </script>
All done! Start your app by running:
1npm start
Now, if you open up http://localhost:3000 in two (or more) tabs in your browser, you should see that liking a post in one instantly reflects in the other. Also, because of our console.log
statement placed earlier, you’ll see the event is logged:
In this article, we’ve seen how Pusher’s publish-subscribe messaging system makes it straightforward to implement a realtime view of activity on a particular post. Of course, this is just a starting point; we look forward to seeing all the great things you’ll build.