Build a realtime comments feature with Nest.js

Introduction

In this article, I will show you how to simply add realtime functionality to a Nest.js application by leveraging the powerful features offered by Pusher. Nest.js is a sever-side Node framework which leverages the power of TypeScript. It’s progressive and scales with whatever size of project you throw at it.

Typescript is the modern style of writing JavaScript, making it easy to debug and organize code base for very large and complicated apps. The obvious difference is that TypeScript adds type information to the code.

Although there are a handful of libraries to develop web applications in Node.js, mostly with JavaScript, here I will introduces a robust Node.js framework called Nest.js. It introduces a new way of building scalable applications by using TypeScript.

Despite being built with TypeScript, Nest.js still preserves compatibility with pure JavaScript. This means you can still write pure JavaScript while developing applications with Nest.js.

What we will build in this article will keep you acquainted with the essential building blocks of a Nest.js application. And you can easily improve on this and explore some other structures and architecture of Nest.js.

Why Nest.js

Nest.js focuses on solving the issues of Architecture on the server-side. According to the documentation here, Nest.js aims to provide an applications architecture out of the box. This will allow developers to seamlessly create highly scalable, loosely coupled and easily maintainable applications.

Building block of Nest.js

Before building with Nest.js, lets have a quick overview of the important building blocks used when building applications:

  • Modules: The @Module() decorator provides metadata, which Nest uses to organize the application structure.
  • Controllers: The controllers layer is responsible for handling incoming requests, and return a response to the client.
  • Components: Almost everything is a component – Service, Repository, Factory, Helper [...] and they can be injected into controllers or into other components through constructor.

You can read more about these here.

What we’ll build

We are going to create a very simple commenting system, where a user can post comments and see the posted message in realtime. To simplify things, there won’t be any need to persist posted comments into the database. We will basically emit an event with added comment(s), subscribe to the channel and update data in realtime.

realtime-comments-nestjs-demo

Install Node

Nest.js is basically a Node.js appplication and, as such, it will require Node and NPM to install the required dependency. Download Node and Npm here.

Set up the application

We will set up Nest.js applications quickly by cloning the starter project on GitHub:

    git clone https://github.com/nestjs/typescript-starter.git nest-comment

This will download a copy of the starter project in a local directory nest-comment. Now you can change directory into the new folder, install all the required dependencies and finally run the application:

1// change directory
2      cd nest-comment
3
4    // install dependencies
5      npm install

Running the application

After installing all dependencies, we now can start the application:

    npm run start

Then navigate to the browser and open localhost:300:

realtime-comments-nestjs-hello-world

Pusher setup

Register for a free pusher account, if you don’t have one already. Then go ahead and create an app from your dashboard and copy out the credentials as you will be required to use them later in this tutorial.

realtime-comments-nestjs-create-app

We are building a JavaScript frontend and backend so we choose JavaScript and Node.js respectively.

Create first controller

Controllers in Nest.js are responsible for handling incoming HTTP requests and returning a response to the client, just like every other HTTP framework. The starter project we just downloaded has a controller already created within ./src/modules/app.controller.ts. For the purpose of this tutorial, we will create a basic controller to handle the necessary requests for our application to function as specified.

To have a proper folder structure, create a new directory for comments ./src/modules/comments.

Now create the controller ./src/modules/comments/comments.controller.ts and paste the following code in it:

1import { Controller, Get, Post, Body, Req, Res } from '@nestjs/common';
2    import { Comment } from './interface/comment';
3    import { CommentService } from './comments.service';
4
5    @Controller('comments')
6    export class CommentsController {
7        constructor(private commentsService: CommentService){}
8        @Get()
9        getComments(@Req() req, @Res() res, err) {
10            res.render('index');
11        }
12
13        @Post()
14        createComment(@Res() res, @Body() comment: Comment) {
15            this.commentsService.create(comment);
16            res.status(201).send('created');
17        }
18    }

In the code above, we only imported modules necessary to create a basic controller for a Nest.js application. The metadata attached to the class allows Nest.js to know how the controller needs to be mapped to the appropriate routes. To do this, we are using the decorators @Controller('comments') , where comments represent a prefix for each route registered in the class.

In order to define the syntax that needs to be adhered to when parameters are being inputted by users, we created and included a TypeScript class. This helps provides a standard structure that should be followed if you have more than one property that needs to be used. This is basic and has only one property message:

1// .src/modules/comments/interface/comment.ts
2
3    export class Comment {
4        message: string;
5        constructor(message: string) {
6            this.message  = message;
7        }
8    }

Also a CommentService class was imported and injected into the controller through the constructor. This service is a component in Nest.js. It handles any complex tasks instead of them being handled by the controller. We will create this service in a bit.

Install the following node modules:

    npm install body-parser pusher ejs 

Create component

Components are plain TypeScript class decorators. It plays, amongst other things, the role of abstracting the business logic away from the controller.

As evident from the code below, the service created for this application contains one method, create(), which is responsible for creating comments. Lets create a component ./src/modules/comments/comments.service.``ts:

1import {Component, OnModuleInit } from '@nestjs/common';
2    import { Comment } from './interface/comment';
3
4    @Component()
5    export class CommentService implements OnModuleInit {
6
7        create(comment: Comment) {
8        ...
9        }
10    }

Realtime features on the server

We want to send the payload to all connected clients when an item is created. Here, we initialise Pusher with the required credentials obtained from the application dashboard:

1create(comment: Comment) {
2            const Pusher = require('pusher');
3
4            var pusher = new Pusher({
5                appId: 'YOUR_PUSHER_APP_ID',
6                key: 'YOUR_PUSHER_APP_KEY',
7                secret: 'YOUR_PUSHER_SECRET',
8                cluster: 'YOUR_CLUSTER',
9                encrypted: true
10              });
11
12              pusher.trigger('comment', 'comment_data', comment);
13        }

Lastly, to make both the controller and the component created available, we need to import and add them to the root module ./src/modules/app.module.ts . Open it and fill with this:

1import { Module } from '@nestjs/common';
2    import { AppController } from './app.controller';
3    import { CommentsController } from './comments/comments.controller';
4    import { CommentService } from './comments/comments.service';
5    @Module({
6      modules: [],
7      controllers: [AppController, CommentsController],
8      components: [CommentService],
9    })
10    export class ApplicationModule {}

Set up the views

At the moment, we have completed setting up the backend of this application as most of the logic has been handled by nest’s Controller and Component. To render the content within the view, we install the EJS module earlier on for this purpose. EJS is a JavaScript templating library. Let’s configure our application to use EJS. Open ./src/server.ts:

1import { NestFactory } from '@nestjs/core';
2    import { ApplicationModule } from './modules/app.module';
3    import * as bodyParser from 'body-parser';
4    import * as Express from 'express';
5
6    let server = new Express();
7
8    async function bootstrap() {
9        const app = await NestFactory.create(ApplicationModule, server);
10        app.use(bodyParser.json());
11        server.set('view engine', 'ejs');
12        await app.listen(3000);
13    }
14    bootstrap();

To configure our application to use EJS, we imported Express and created an instance with the name server. This was use to set EJS as the view engine as evident from the code above. By default the NestFactory``.create() method takes an Express instance as a second argument. This is deliberate as Nest.js allows developers to have full control of the Express instance life cycle.

Next, within the root directory make a new folder views and create an index.``ejs file within it:

1// ./views/index.ejs
2    <!DOCTYPE html>
3    <html lang="en">
4    <head>
5        <meta charset="UTF-8">
6        <meta name="viewport" content="width=device-width, initial-scale=1.0">
7        <meta http-equiv="X-UA-Compatible" content="ie=edge">
8        <link rel="stylesheet" href="//maxcdn.bootstrapcdn.com/bootstrap/3.2.0/css/bootstrap.min.css">        
9        <script src="https://cdnjs.cloudflare.com/ajax/libs/vue/2.5.13/vue.js"></script>
10        <script src="https://cdnjs.cloudflare.com/ajax/libs/pusher/4.2.2/pusher.js"></script>
11        <script src="https://cdnjs.cloudflare.com/ajax/libs/axios/0.17.1/axios.js"></script>
12        <title>Document</title>
13    </head>
14    <body>
15        <nav class="navbar navbar-default" role="navigation">
16            <div class="container-fluid">
17
18                <div class="navbar-header">
19                    <a class="navbar-brand" href="#">
20                        <span class="glyphicon glyphicon glyphicon-tree-deciduous"></span>
21                        Nest Js
22                    </a>
23                </div>
24
25            </div>
26        </nav>
27        <div class="container" id="app">
28            <main>            
29                <div class="row">
30                    <div class="col-md-8">
31                        <form method="post" action="/comments">
32                            <div class="form-group">
33                                <input type="text" class="form-control" name="message" placeholder="Type your comments">
34                            </div>
35
36                            <div class="form-group">
37                                <button class="btn btn-success" type="submit">Submit</button>
38                            </div>
39                        </form>
40                    </div>
41                </div> 
42            </main>
43        </div>
44        <script>
45        //.....
46        //.....
47        //.....
48        </script>
49    </body>
50    </html>

The code imports Vue, Pusher, and Axios. Vue will simplify how we interact with the DOM while the Pusher client helps us to connect with our Pusher instance on the server. Axios will be used to send the HTTP Post request to the /comments endpoint.

Update the script tag before the body closing tag to show this log:

1new Vue({
2                el: '#app',
3                data() {
4                    return {
5                        comments: [],
6                        comment: {
7                            message: ''
8                        }
9                    }
10                },
11                created() {
12                    let pusher = new Pusher('PUSHER_KEY', {
13                        cluster: 'PUSHER_CLUSTER',
14                        encrypted: true
15                    });
16                    const channel = pusher.subscribe('comment');
17                    channel.bind('comment_data', data => {
18                        this.comments.push(data);
19                    });
20                },
21                methods: {
22                    submitComment() {
23                        axios.post('/comments', this.comment).then((data) => {
24                            console.log(data)
25                        })
26                    }
27                }
28            })

As you can see, the created lifecycle method listens for changes and updates our DOM based on the new comments emitted by the server. The submitComment method is triggered every time we submit the comment form. It uses Axios to send the comment payload to the server.

Run the application

Stop and run the sever again. Then navigate to this endpoint http://localhost:3000/comments:

realtime-comments-nextjs-hello-world

This basically renders the content within index.ejs. It contains a header bar and form to post comments.

Remember we already subscribed to the channel that emits data once comments are being posted. To display the comments, we will iterate over the comments:

1<!DOCTYPE html>
2    <html lang="en">
3    <head>
4    ...
5    </head>
6    <body>
7    <nav class="navbar navbar-default" role="navigation">
8        ...
9    </nav>
10
11    <div class="container" id="app">
12        <main>
13            <div class="jumbotron">
14                <h3>Comments</h3>
15                <ul>
16                    <li v-for="comment in comments">
17                        {{comment.message}}
18                    </li>
19                </ul>
20            </div>
21        </main>
22
23        <div class="row">
24            ...
25         </div>
26    </div>
27    </body>
28    </html>
29#

Stop and run the sever again. Then navigate to this endpoint http://localhost:3000/comments:

https://www.youtube.com/watch?v=xaMUyaYUvoQ

Now, you can post a comment and see it updated in realtime.

Final thoughts

Nest.js introduces a new approach to building web applications in Node.js. The usage of TypeScript helps to manage and create a properly structured application. As we saw in this article, with the help of Pusher, we were able to add realtime functionality to Nest.js. The source code for the app can be found on GitHub.