Build live comments with sentiment analysis using Nest.js
You will need Node and npm installed on your machine. A basic understanding of TypeScript and Node will help you get the most out of this tutorial.
Introduction
Interestingly, one of the most important areas of a blog post is the comment section. This plays an important role in the success of a post or an article, as it allows proper interaction and participation from readers. This makes it inevitable for every platform with a direct comments system to handle it in realtime.
In this post, we’ll build an application with a live comment feature. This will happen in realtime as we will tap into the infrastructure made available by Pusher. We will also use sentiment analysis to detect the mood of a person based on the words they use in their comments.
A sneak-peek into what we will build in this post:
https://www.youtube.com/watch?v=WRJdQIqiKo0&
Prerequisites
A basic understanding of TypeScript and Node.js will help you get the best out of this tutorial. I assume that you already have Node and npm installed, if otherwise quickly check Node.js and npm for further instructions and installation steps.
Here is a quick overview of the technologies that we will be using in this post.
-
Nest.js: a progressive framework for building efficient and scalable server-side applications; built to take the advantage of modern JavaScript but still preserves compatibility with pure JavaScript.
-
Pusher: a Node.js client to interact with the Pusher REST API
-
Axios: a promise-based HTTP client that works both in the browser and Node.js environment.
-
Sentiment: sentiment is a module that uses the AFINN-165 wordlist and Emoji Sentiment Ranking to perform sentiment analysis on arbitrary blocks of input text.
-
Vue.js: Vue is a progressive JavaScript frontend framework for building web applications.
Setting up the application
The simplest way to set up a Nest.js application is to install the starter project on Github using Git. To do this, let’s run a command that will clone the starter repository into a new project folder named live-comments-nest
on your machine. Open your terminal or command prompt and run the command below:
$ git clone https://github.com/nestjs/typescript-starter.git live-comments-nest
Go ahead and change directory into the newly created folder and install all the dependencies for the project.
// change directory
cd live-comments-nest
// install dependencies
npm install
Running application
Start the application with :
npm start
The command above will start the application on the default port used by Nest.js. Open your browser and navigate to http://localhost:3000. You should see a page with a welcome message.
Installing server dependencies
Run the command below to install the server dependencies required for this project.
npm install ejs body-parser pusher sentiment
-
ejs: this is a simple templating language for generating HTML markup with plain JavaScript.
-
Body-parser: a middleware used for extracting the entire body portion of an incoming request stream and exposing it on
req.body
. -
Pusher: a Node.js client to interact with the Pusher REST API
-
Sentiment: a Node.js module to perform sentiment analysis.
Signing up with Pusher
Head over to Pusher and sign up for a free account.
Create a new app by selecting Channels apps on the sidebar and clicking Create Channels app button on the bottom of the sidebar:
Configure an app by providing basic information requested in the form presented. You can also choose the environment you intend to integrate Pusher with, for a better setup experience:
You can retrieve your keys from the App Keys tab:
Bootstrap the application
Nest.js uses the Express library and therefore, favors the popular MVC pattern.
To set this up, open up the main.ts
file and update it with the content below:
// ./src/main.ts
import { NestFactory } from '@nestjs/core';
import { AppModule } from './app.module';
import * as bodyParser from 'body-parser';
import * as express from 'express';
import * as path from 'path';
async function bootstrap() {
const app = await NestFactory.create(AppModule);
app.use(express.static(path.join(__dirname, 'public')));
app.set('views', __dirname + '/views');
// set ejs as the view engine
app.set('view engine', 'ejs');
await app.listen(3000);
}
bootstrap();
This is the entry point of the application and necessary for bootstrapping Nest.js apps. I have included the Express module, path and set up ejs as the view engine for the application.
Building the homepage
As configured within main.ts
file, the views
folder will hold all the template for this application. Now let’s go ahead and create it within the src
folder. Once you are done, create a new file named index.ejs
right inside the newly created views
folder and update the content with:
// ./src/views/index.ejs
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<meta http-equiv="X-UA-Compatible" content="ie=edge">
<link rel="stylesheet" href="https://maxcdn.bootstrapcdn.com/bootstrap/3.3.7/css/bootstrap.min.css">
<link rel="stylesheet" href="/style.css">
<script src="https://cdnjs.cloudflare.com/ajax/libs/vue/2.5.13/vue.js"></script>
<title>Realtime comments</title>
</head>
<body>
<div id="app">
<div class="message">
<h2 class="text-center">Interesting post</h2>
<hr>
<p>
Once you're done creating the quality content,
you still have the challenge of presenting it
that clearly dictates what your blog is about.
Images, text, and links need to be shown off just
right -- otherwise, readers might abandon your content,
if it's not aesthetically showcased in a way that's both
appealing and easy to follow.
</p>
</div>
<div class="comments">
<div class="comment-wrap">
<div class="photo">
<div class="avatar" style="background-image: url('http://res.cloudinary.com/yemiwebby-com-ng/image/upload/v1525202285/avatar_xcah9z.svg')"></div>
</div>
<div class="comment-block">
<textarea name="" id="" cols="30" rows="3" placeholder="Add comment and hit ENTER" @Keyup.enter="postComment"></textarea>
</div>
</div>
<div v-for="comment in comments">
<div class="comment-wrap">
<div class="photo">
<div class="avatar" style="background-image: url('http://res.cloudinary.com/yemiwebby-com-ng/image/upload/v1525202285/avatar_xcah9z.svg')"></div>
</div>
<div class="comment-block">
<p class="comment-text"> <b>{{ comment.message }}</b> </p>
<div class="bottom-comment">
<div class="comment-mood">{{comment.mood}}</div>
</div>
</div>
</div>
</div>
</div>
</div>
<script src="https://cdnjs.cloudflare.com/ajax/libs/axios/0.16.2/axios.js"></script>
<script src="https://js.pusher.com/4.1/pusher.min.js"></script>
<script src="/main.js"></script>
</body>
</html>
This will serve as the homepage for the application, hence the need for it to be well structured. A quick look at some of the included files and other elements on this page.
-
Firstly, we included a link to the Bootstrap CDN file to add some default styling and layout to our application. We also added a custom stylesheet for further styling. We will create this stylesheet in the next section. Also included in a
<script>
tag just before the page title is a CDN file for Vue.js. -
We used an event handler to listen to keyboard events using key modifiers aliases made available by Vue.js. This process will be discussed later in the tutorial.
-
And finally, we included CDN file each for
Axios
,Pusher
and then proceeded to add a custom script file namedmain.js
. To set up this file, go ahead and create apublic
folder within thesrc
folder in the application and create themain.js
file within it.
Stylesheet
To set up this stylesheet, locate the public
folder and create a new file named style.css
within it. Next, open the file and paste the code below:
// ./src/public/style.css
html, body {
background-color: #f0f2fa;
font-family: "PT Sans", "Helvetica Neue", "Helvetica", "Roboto", "Arial", sans-serif;
color: #555f77;
-webkit-font-smoothing: antialiased;
}
input, textarea {
outline: none;
border: none;
display: block;
margin: 0;
padding: 0;
-webkit-font-smoothing: antialiased;
font-family: "PT Sans", "Helvetica Neue", "Helvetica", "Roboto", "Arial", sans-serif;
font-size: 1.5rem;
color: #555f77;
}
input::-webkit-input-placeholder, textarea::-webkit-input-placeholder {
color: #ced2db;
}
input::-moz-placeholder, textarea::-moz-placeholder {
color: #ced2db;
}
input:-moz-placeholder, textarea:-moz-placeholder {
color: #ced2db;
}
input:-ms-input-placeholder, textarea:-ms-input-placeholder {
color: #ced2db;
}
.message {
margin: 2.5rem auto 0;
max-width: 60.75rem;
padding: 0 1.25rem;
font-size: 1.5rem;
}
.comments {
margin: 2.5rem auto 0;
max-width: 60.75rem;
padding: 0 1.25rem;
}
.comment-wrap {
margin-bottom: 1.25rem;
display: table;
width: 100%;
min-height: 5.3125rem;
}
.photo {
padding-top: 0.625rem;
display: table-cell;
width: 3.5rem;
}
.photo .avatar {
height: 2.25rem;
width: 2.25rem;
border-radius: 50%;
background-size: contain;
}
.comment-block {
padding: 1rem;
background-color: #fff;
display: table-cell;
vertical-align: top;
border-radius: 0.1875rem;
box-shadow: 0 1px 3px 0 rgba(0, 0, 0, 0.08);
}
.comment-block textarea {
width: 100%;
resize: none;
}
.comment-text {
margin-bottom: 1.25rem;
}
.bottom-comment {
color: #acb4c2;
font-size: 0.875rem;
}
.comment-mood {
float: left;
font-size: 30px;
}
Handling routes
The controller layer in Nest.js is responsible for receiving an incoming request and returning the appropriate response to the client. Nest uses a controller metadata @Controller
to map routes to a specific controller. The starter project already contains a controller by default. We will make use of this in order to render the home for this app. Open ./src/app.controller.ts
and edit as shown below:
// ./src/app.controller.ts
import { Get, Controller, Res } from '@nestjs/common';
@Controller()
export class AppController {
@Get()
root(@Res() res) {
res.render('index');
}
}
This controller will ensure that Nest.js maps every /
route to the index.ejs
file.
Create a Vue instance
Earlier, we created main.js
file within the public
folder and included it on our homepage. To get things a little bit organized, we will create Vue instance within this file and bind it to a div
element with an id of #app
:
// ./src/public/main.js
new Vue({
el: '#app',
data: {
comments: []
},
...
})
We have also declared the initial value for comments
as an empty array inside the data
options.
Restart the development server if it is currently running. Check your page on http://localhost:3000. You should see:
This is a basic layout of what our page will look like for now. This contains a sample post and a comment box for readers to enter their comments and react to the blog post. In the next section, we’ll start a process to add some reactivity and enable realtime functionality for the comments.
Create the Comment controller
Create a new folder named comments
in the src
folder and create a new file called comment.controller.ts
within it. In the newly created file, paste in the following code:
// ./src/comments/comment.controller.ts
import { Controller, Post, Res, Body, HttpStatus } from '@nestjs/common';
import { CommentService } from './comment.service';
@Controller('comment')
export class CommentController {
constructor(private commentService: CommentService){}
@Post()
postMessage(@Res() res, @Body() data ) {
this.commentService.addComment(data)
res.status(HttpStatus.OK).send("Comment posted successfully")
}
}
This controller will handle the comments posted by readers and pass it to a service specifically created and imported for that purpose.
As shown above, we imported CommentService
and injected it into the controller through the constructor. As recommended by Nest, a controller should handle only HTTP requests and abstract any complex logic to a service.
Realtime comment service with Pusher
CommentController
depends on a service named CommentService
to receive the comment
submitted by readers and publish it to a designated channel for the client side to listen and subscribe to. Let’s create this service. Go to the comments
folder and create a new file named comment.service.ts
within it and then paste the code below into newly created file:
// ./src/comments/comment.service.ts
import { Component } from '@nestjs/common';
const Sentiment = require('sentiment');
@Component()
export class CommentService {
addComment(data) {
const Pusher = require('pusher');
const sentiment = new Sentiment();
const sentimentScore = sentiment.analyze(data.comment).score;
const payload = {
message: data.comment,
sentiment: sentimentScore
}
var pusher = new Pusher({
appId: 'YOUR_APP_ID',
key: 'YOUR_API_KEY',
secret: 'YOUR_SECRET_KEY',
cluster: 'CLUSTER',
encrypted: true
});
pusher.trigger('comments', 'new-comment', payload);
}
}
Here we received the comment payload and then imported and used the sentiment
module to calculate the overall sentiment score of the comment. Next, we construct a payload
object with the sentiment property included.
Finally, we initialized Pusher with the required credentials so as to trigger an event named new-comment
through a comments
channel. Don’t forget to replace YOUR_APP_ID
, YOUR_API_KEY
, YOUR_SECRET_KEY
and CLUSTER
with the right credentials obtained from your dashboard.
Register the component and the controller
At the moment, our application doesn’t recognize the newly created controller and service. Let’s change this by editing our module file 'app.module.ts'
; putting the controller into the 'controller'
array and service into 'components'
array of the '@Module()
decorator respectively.
// ./src/app.module.ts
import { CommentService } from './comments/comment.service';
import { CommentController } from './comments/comment.controller';
import { Module } from '@nestjs/common';
import { AppController } from './app.controller';
@Module({
imports: [],
controllers: [AppController, CommentController],
components: [CommentService],
})
export class AppModule {}
Post comments to the server
In order to post a comment to the server, we created a new method postComment()
within ./src/public/main.js
file:
// ./src/public/main.js
new Vue({
el: '#app',
data: {
...
},
methods: {
postComment(event) {
const message = event.target.value;
if (event.keyCode === 13 ) {
const comment = {
comment: message,
};
event.target.value = "";
axios.post('/comment', comment).then( data => { console.log(data)});
}
}
}
})
On the postComment()
event handler, we construct a comment
object, which holds the message posted by readers and then removes all the content of the textarea
. Finally, we used Axios
to make a POST HTTP request to the /comment
endpoint, passing the comment object created as a payload.
Update the page with a live comment
After comments are being posted by readers, we need to listen for the Pusher event and subscribe to the channel. This will give us access to the payload required to update the page. Open main.js
and update it with the code below:
// ./src/public/main.js
const SAD_EMOJI = [55357, 56864];
const HAPPY_EMOJI = [55357, 56832];
const NEUTRAL_EMOJI = [55357, 56848];
new Vue({
el: '#app',
data: {
...
},
created() {
let pusher = new Pusher('YOUR_API_KEY', {
cluster: 'CLUSTER',
encrypted: true
});
const channel = pusher.subscribe('comments');
channel.bind('new-comment', data => {
const analysis = data.sentiment > 0 ? HAPPY_EMOJI : (data.sentiment === 0 ? NEUTRAL_EMOJI : SAD_EMOJI);
const response = {
message: data.message,
mood: String.fromCodePoint(...analysis)
}
this.comments.push(response);
});
},
methods: {
...
}
})
First, we added constants for SAD_EMOJI
, HAPPY_EMOJI
and NEUTRAL_EMOJI
. Each of these constants is an array of the code points required for a particular sentiment emoji.
Next, we established a connection to Pusher Channels using the Key
and Cluster
obtained from our dashboard. We then proceeded to subscribe to the comments
channel we created earlier and listen for an event new-comment
.
We also used the sentiment
score obtained from the payload on our comments
channel to set the mood of the reader to either happy, sad or neutral using the constants that were defined earlier.
Finally, we used String.fromCodePoint() method to get the emoji from the code points defined in our constants earlier and the response
object containing the message
and mood
of the reader to the comments
array.
Testing the application
Restart the development server if it is currently running. Check your page on http://localhost:3000
https://www.youtube.com/watch?v=WRJdQIqiKo0&
Conclusion
The use case of a comment widget is quite enormous. As shown in this post, you can easily get readers to interact and react with comments in realtime.
I hope you found this tutorial helpful and would love to apply the knowledge gained here to easily set up your own application using Pusher. You can check the demo source code of this tutorial on Github.
8 May 2018
by Christian Nwamba