Build live comments with sentiment analysis using Nest.js

Introduction

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.

1// change directory
2    cd live-comments-nest
3    
4    // install dependencies
5    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 localhost:3000. You should see a page with a welcome message.

nest-hello-world

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.

nest-phone-pusher-1

Create a new app by selecting Channels apps on the sidebar and clicking Create Channels app button on the bottom of the sidebar:

nest-phone-pusher-2

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:

nest-phone-pusher-3

You can retrieve your keys from the App Keys tab:

nest-phone-pusher-4

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:

1// ./src/main.ts
2    
3    import { NestFactory } from '@nestjs/core';
4    import { AppModule } from './app.module';
5    
6    import * as bodyParser from 'body-parser';
7    import * as express from 'express';
8    import * as path from 'path';
9    
10    async function bootstrap() {
11      const app = await NestFactory.create(AppModule);
12      app.use(express.static(path.join(__dirname, 'public')));
13       app.set('views', __dirname + '/views');
14       // set ejs as the view engine
15       app.set('view engine', 'ejs');
16      await app.listen(3000);
17    }
18    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:

1// ./src/views/index.ejs
2    
3    <!DOCTYPE html>
4    <html lang="en">
5    <head>
6        <meta charset="UTF-8">
7        <meta name="viewport" content="width=device-width, initial-scale=1.0">
8        <meta http-equiv="X-UA-Compatible" content="ie=edge">
9        <link rel="stylesheet" href="https://maxcdn.bootstrapcdn.com/bootstrap/3.3.7/css/bootstrap.min.css">
10        <link rel="stylesheet" href="/style.css">
11        <script src="https://cdnjs.cloudflare.com/ajax/libs/vue/2.5.13/vue.js"></script>
12        <title>Realtime comments</title>
13    </head>
14    <body>
15        
16        <div id="app">
17            <div class="message">
18                <h2 class="text-center">Interesting post</h2>
19                <hr>
20                <p>
21                   Once you're done creating the quality content,
22                    you still have the challenge of presenting it 
23                    that clearly dictates what your blog is about.
24                     Images, text, and links need to be shown off just 
25                     right -- otherwise, readers might abandon your content, 
26                     if it's not aesthetically showcased in a way that's both 
27                     appealing and easy to follow.
28                 </p>
29            </div>
30            <div class="comments">
31                <div class="comment-wrap">
32                    <div class="photo">
33                        <div class="avatar" style="background-image: url('http://res.cloudinary.com/yemiwebby-com-ng/image/upload/v1525202285/avatar_xcah9z.svg')"></div>
34                    </div>
35                    <div class="comment-block">
36                        <textarea name="" id="" cols="30" rows="3" placeholder="Add comment and hit ENTER" @Keyup.enter="postComment"></textarea>
37                    </div>
38                </div>
39                
40                
41                <div v-for="comment in comments">
42                    <div class="comment-wrap">
43                        <div class="photo">
44                            <div class="avatar" style="background-image: url('http://res.cloudinary.com/yemiwebby-com-ng/image/upload/v1525202285/avatar_xcah9z.svg')"></div>
45                        </div>
46                        <div class="comment-block">
47                            <p class="comment-text"> <b>{{ comment.message }}</b> </p>
48                            <div class="bottom-comment">
49                                <div class="comment-mood">{{comment.mood}}</div>
50                            </div>
51                        </div>
52                    </div>
53                </div>
54            </div>
55        </div>
56        
57        <script src="https://cdnjs.cloudflare.com/ajax/libs/axios/0.16.2/axios.js"></script>
58        <script src="https://js.pusher.com/4.1/pusher.min.js"></script>
59        <script src="/main.js"></script>
60    </body>
61    </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 named main.js. To set up this file, go ahead and create a public folder within the src folder in the application and create the main.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:

1// ./src/public/style.css
2    
3    html, body {
4        background-color: #f0f2fa;
5        font-family: "PT Sans", "Helvetica Neue", "Helvetica", "Roboto", "Arial", sans-serif;
6        color: #555f77;
7        -webkit-font-smoothing: antialiased;
8      }
9      
10      input, textarea {
11        outline: none;
12        border: none;
13        display: block;
14        margin: 0;
15        padding: 0;
16        -webkit-font-smoothing: antialiased;
17        font-family: "PT Sans", "Helvetica Neue", "Helvetica", "Roboto", "Arial", sans-serif;
18        font-size: 1.5rem;
19        color: #555f77;
20      }
21      input::-webkit-input-placeholder, textarea::-webkit-input-placeholder {
22        color: #ced2db;
23      }
24      input::-moz-placeholder, textarea::-moz-placeholder {
25        color: #ced2db;
26      }
27      input:-moz-placeholder, textarea:-moz-placeholder {
28        color: #ced2db;
29      }
30      input:-ms-input-placeholder, textarea:-ms-input-placeholder {
31        color: #ced2db;
32      }
33      .message {
34        margin: 2.5rem auto 0;
35        max-width: 60.75rem;
36        padding: 0 1.25rem;
37        font-size: 1.5rem;
38      }
39      
40      .comments {
41        margin: 2.5rem auto 0;
42        max-width: 60.75rem;
43        padding: 0 1.25rem;
44      }
45      
46      .comment-wrap {
47        margin-bottom: 1.25rem;
48        display: table;
49        width: 100%;
50        min-height: 5.3125rem;
51      }
52      
53      .photo {
54        padding-top: 0.625rem;
55        display: table-cell;
56        width: 3.5rem;
57      }
58      .photo .avatar {
59        height: 2.25rem;
60        width: 2.25rem;
61        border-radius: 50%;
62        background-size: contain;
63      }
64      
65      .comment-block {
66        padding: 1rem;
67        background-color: #fff;
68        display: table-cell;
69        vertical-align: top;
70        border-radius: 0.1875rem;
71        box-shadow: 0 1px 3px 0 rgba(0, 0, 0, 0.08);
72      }
73      .comment-block textarea {
74        width: 100%;
75        resize: none;
76      }
77      
78      .comment-text {
79        margin-bottom: 1.25rem;
80      }
81      
82      .bottom-comment {
83        color: #acb4c2;
84        font-size: 0.875rem;
85      }
86      
87      .comment-mood {
88        float: left;
89        font-size: 30px;
90      }

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:

1// ./src/app.controller.ts
2    
3    import { Get, Controller, Res } from '@nestjs/common';
4    @Controller()
5    export class AppController {
6      @Get()
7      root(@Res() res) {
8        res.render('index');
9      }
10    }

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:

1// ./src/public/main.js
2    
3    new Vue({
4        el: '#app',
5        data: {
6            comments: []
7        },
8        ...
9    })

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 localhost:3000. You should see:

nest-sentiment-comments-stage-1

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:

1// ./src/comments/comment.controller.ts
2    
3    import { Controller, Post, Res, Body, HttpStatus } from '@nestjs/common';
4    import { CommentService } from './comment.service';
5    
6    @Controller('comment')
7    export class CommentController {
8        constructor(private commentService: CommentService){}
9      @Post()
10      postMessage(@Res() res, @Body() data ) {
11          this.commentService.addComment(data)
12          res.status(HttpStatus.OK).send("Comment posted successfully")
13      }
14    }

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:

1// ./src/comments/comment.service.ts
2    
3    import { Component } from '@nestjs/common';
4    const Sentiment = require('sentiment');
5    @Component()
6    export class CommentService {
7        addComment(data) {
8            const Pusher = require('pusher');
9            const sentiment = new Sentiment();
10            const sentimentScore = sentiment.analyze(data.comment).score;
11            const payload = {
12                message: data.comment,
13                sentiment: sentimentScore
14            }
15            var pusher = new Pusher({
16                appId: 'YOUR_APP_ID',
17                key: 'YOUR_API_KEY',
18                secret: 'YOUR_SECRET_KEY',
19                cluster: 'CLUSTER',
20                encrypted: true
21              });
22              pusher.trigger('comments', 'new-comment', payload);
23        }
24    }

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.

1// ./src/app.module.ts
2    
3    import { CommentService } from './comments/comment.service';
4    import { CommentController } from './comments/comment.controller';
5    import { Module } from '@nestjs/common';
6    import { AppController } from './app.controller';
7    @Module({
8      imports: [],
9      controllers: [AppController, CommentController],
10      components: [CommentService],
11    })
12    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:

1// ./src/public/main.js
2    
3    new Vue({
4        el: '#app',
5        data: {
6            ...
7        },
8        methods: {
9            postComment(event) {
10                const message = event.target.value;
11                if (event.keyCode === 13 ) {
12                    const comment = {
13                        comment: message,
14                    };
15                    event.target.value = "";
16                    axios.post('/comment', comment).then( data => { console.log(data)});
17                }
18            }
19        }
20    })

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:

1// ./src/public/main.js
2    
3    const SAD_EMOJI = [55357, 56864];
4    const HAPPY_EMOJI = [55357, 56832];
5    const NEUTRAL_EMOJI = [55357, 56848];
6    
7    new Vue({
8        el: '#app',
9        data: {
10           ...
11        },
12        created() {
13            let pusher = new Pusher('YOUR_API_KEY', {
14                cluster: 'CLUSTER',
15                encrypted: true
16            });
17            
18            const channel = pusher.subscribe('comments');
19            channel.bind('new-comment', data => {
20                const analysis = data.sentiment > 0 ? HAPPY_EMOJI : (data.sentiment === 0 ? NEUTRAL_EMOJI : SAD_EMOJI);
21                const response = {
22                    message: data.message,
23                    mood: String.fromCodePoint(...analysis)
24                }
25                this.comments.push(response);
26            });
27        },
28        methods: {
29            ...
30        }
31    })

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