Build a real-time chat application with Websocket, Socket.IO, Redis, and Docker in NestJS.

Phat Vo
10 min readSep 14, 2020

--

Nowadays, life is hectic and the world is running in real-time.
Every news, data, and information ..etc are transforming very fast and seem like immediately. Not only journalist, retail, or financial majors but there is also a range of majors that transform their data, news in a realtime communicate way.
In the world of programming, we all know chat application is a realtime system. Which is an application to communicate between the users in immediate interaction like FB Messenger ..etc.
So, today we are going to build a chat realtime application with Websocket, Redis, and Docker in NestJS Framework.

This article for those who familiar with NestJS Framework, Typescript and tend to gravitate towards OOP. If you want to find more about design patterns, design architectures. So then, I’ve some other articles introduced some aspects of programming using NestJS Framework as those links show below.

We will build a realtime chat application with NestJS from scratch follow to this tutorial. Before going to capturing anything that you need to know about WebSocket, Socket.IO, and Redis Pub/Sub. I’ll have a recap what’s WebSocket, Socket.IO, Redis and why using Redis with WebSocket in our backend application.

What’s WebSocket?

WebSocket is a computer communications protocol, providing full-duplex communication channels over a single TCP connection. The WebSocket protocol was standardized by the IETF as RFC 6455, and the WebSocket API in Web IDL is being standardized by the W3C. WebSocket is distinct from HTTP.

What’s Socket.IO?

Socket.IO is a JavaScript library for realtime web applications. It enables realtime, bi-directional communication between web clients and servers. It has two parts: a client-side library that runs in the browser, and a server-side library for Node.js.

What’s Redis Pub/Sub?

Redis Pub/Sub implements the messaging system where the senders (in redis terminology called publishers). The pub-sub pattern allows senders of messages, called publishers to publish messages to receivers called subscribers through a channel without knowledge of which subscribers exist — if any. All subscribers exist at the time the message received can receive the message at the same time.

Why use Redis for WebSocket communication?

Actually, using Websocket is enough as the transport protocol to communicate between the clients. But there is some scenario that makes our chat realtime application struggle.
Let’s consider a chat application. When a user first connects, a corresponding WebSocket connection is created within the application (WebSocket server) and it is associated with the specific application instance. This WebSocket connection is what empowers the medium to enables us to broadcast chat messages between users. Now, if a new user comes in, they may be connected to a new instance. So we have a scenario where different users (hence their respective WebSocket connections) are associated with different instances. As a result, they will not be able to exchange messages with each other — this is unacceptable, even for our toy chat application. So, we need to use Redis Pub/Sub events to makes those things run-in smoothly.

OK! perhaps you have to catch the ideas of those services we have mentioned. So, let’s going to implement those things in our application.

  • Install Nest CLI in your local machine via this command: npm i -g @nestjs/cli
  • Create an application called nest-chat-realtime by using Nest CLI: nest new nest-chat-realtime
  • Go to the folder where your application situated: cd nest-chat-realtime
  • Install the core and support dependencies by using npm : npm i --save @nestjs/core @nestjs/common rxjs reflect-metadata
  • Install platform-express by running the command: npm install @nestjs/platform-express
  • Run the command npm install to makes sure all the dependencies are bootstrapped.
  • Start the application in development mode: npm run start:dev

After our application running successfully, so we can start to implement the Websocket adapter into the application. Which is the Redis adapter, if you are not familiar with Websocket adapters in the NestJS framework here is an article that might approach appropriate to you (https://docs.nestjs.com/websockets/adapter).

  • Install socket.io-redis package by running this command: npm i --save socket.io-redis
  • Install nest flatform socket by running this command: npm i --save @nest/platform-socket.io
  • Install nest websocket by using this command: npm i --save @nestjs/websockets
  • Install dot env to empower enables using application configure via .env file by the following commands: npm i --save dotenv and npm i --save @nestjs/config

So, let’s restructure the src folder as figure 1.1 illustrates below.

Figure 1.1: Restructure the file and folder inside `src` to an appropriate architecture.

And let’s go to define the code inside those files as shown above. Following the snippets below.

redis.adapter.ts

We will create an IO server by using a Redis adapter with Redis credentials running on your local machine or Docker.

import { IoAdapter } from '@nestjs/platform-socket.io';
import * as redisIoAdapter from 'socket.io-redis';

export class RedisIoAdapter extends IoAdapter {
createIOServer(port: number): any {
const server = super.createIOServer(port);
const redisAdapter = redisIoAdapter(
{
host: process.env.REDIS_HOST,
port: parseInt(process.env.REDIS_PORT),
});
server.adapter(redisAdapter);
return server;
}
}

message.gateway.ts

We will declare the Websocket gateway into this. There is some subscriber method that helps to listen to the events published from the client as the user joins to a room, the user leaves a room, and user sending the messages. Those clients will use this WebSocket protocol to connect to each other in realtime.

import {
SubscribeMessage,
WebSocketGateway,
OnGatewayInit,
WebSocketServer,
OnGatewayConnection,
OnGatewayDisconnect,
WsResponse,
} from '@nestjs/websockets';
import { Logger } from '@nestjs/common';
import { Socket } from 'socket.io';
import { Server } from 'ws';

@WebSocketGateway({ namespace: '/chat' })
export class MessageGateway implements OnGatewayInit, OnGatewayConnection, OnGatewayDisconnect {

@WebSocketServer() server: Server;

private logger: Logger = new Logger('MessageGateway');

@SubscribeMessage('msgToServer')
public handleMessage(client: Socket, payload: any): Promise<WsResponse<any>> {
return this.server.to(payload.room).emit('msgToClient', payload);
}

@SubscribeMessage('joinRoom')
public joinRoom(client: Socket, room: string): void {
client.join(room);
client.emit('joinedRoom', room);
}

@SubscribeMessage('leaveRoom')
public leaveRoom(client: Socket, room: string): void {
client.leave(room);
client.emit('leftRoom', room);
}

public afterInit(server: Server): void {
return this.logger.log('Init');
}

public handleDisconnect(client: Socket): void {
return this.logger.log(`Client disconnected: ${client.id}`);
}

public handleConnection(client: Socket): void {
return this.logger.log(`Client connected: ${client.id}`);
}
}

message.module.ts

We will provide the message-gatway into a module, this working as dependency injection.

import { Module } from '@nestjs/common';
import { MessageGateway } from './message.gateway';

@Module({
imports: [],
controllers: [],
providers: [MessageGateway],
})
export class MessageModule {}

And, to complete the things that we will be adding some configure for message gateway, Redis adapter, and configure environment as global in theapp.module.ts and main.ts files.

app.module.ts

import { Module } from '@nestjs/common';
import { ConfigModule } from '@nestjs/config';
import { MessageModule } from './message-events/message.module';

@Module({
imports: [
ConfigModule.forRoot({
isGlobal: true,
}),
MessageModule,
],
controllers: [],
providers: [],
})
export class AppModule {
}

main.ts

We will use some client codes to trigger our chat realtime application later. So, the client codes will be situated in resource a folder, and NestJS has supported running along with some client codes by using useStaticAssets for a configure.

import { NestFactory } from '@nestjs/core';
import { AppModule } from './app.module';
import { RedisIoAdapter } from './adapters/redis.adapter';
import { NestExpressApplication } from '@nestjs/platform-express';
import { join } from "path";

async function bootstrap() {
const app = await NestFactory.create<NestExpressApplication>(AppModule);
app.useWebSocketAdapter(new RedisIoAdapter(app));
app.useStaticAssets(join(__dirname, '..', 'resource'));
const port = parseInt(process.env.SERVER_PORT);
await app.listen(port);
}

bootstrap();

Our .env file could define some variables as shown as the snippets below. Whereas REDIS_HOST and REDIS_PORT are those Redis credentials using to trigger the Redis adapter with WebSocket.

NODE_ENV=development
SERVER_TIMEOUT=1080000
SERVER_PORT=3004

REDIS_HOST=redis
REDIS_PORT=6379

The next step, we’re going to integrate the Docker in the application by using docker-compose . The docker configures files will be situated at the root of the application folder. So, I have left there some comments for the explanatory in the docker files.

.dockerignore

.git
.gitignore
node_modules/

Dockerfile

# Pull node image from docker hub
FROM node:10-alpine

# Set working directory
RUN mkdir -p /var/www/nest-chat-realtime
WORKDIR /var/www/nest-chat-realtime

# add `/usr/src/app/node_modules/.bin` to $PATH
ENV PATH /var/www/nest-chat-realtime/node_modules/.bin:$PATH
# create user with no password
RUN adduser --disabled-password chat

# Copy existing application directory contents
COPY . /var/www/nest-chat-realtime
# install and cache app dependencies
COPY package.json /var/www/nest-chat-realtime/package.json
COPY package-lock.json /var/www/nest-chat-realtime/package-lock.json

# grant a permission to the application
RUN chown -R chat:chat /var/www/nest-chat-realtime
USER chat

# clear application caching
RUN npm cache clean --force
# install all dependencies
RUN npm install

EXPOSE 3004
CMD [ "npm", "run", "start:dev" ]

docker-compose.yml

version: '3.7'
# all the containers have to declare inside services
services:
# App service
nestchat:
# application rely on redis running
depends_on:
- redis
# this build context will take the commands from Dockerfile
build:
context: .
dockerfile: Dockerfile
# image name
image: nest-chat-realtime-docker
# container name
container_name: nestchat
# always restart the container if it stops.
restart: always
# docker run -t is allow
tty: true
# application port, this is take value from env file
ports:
- "${SERVER_PORT}:${SERVER_PORT}"
# working directory
working_dir: /var/www/nest-chat-realtime
# application environment
environment:
SERVICE_NAME: nestchat
SERVICE_TAGS: dev
REDIS_PORT: ${REDIS_PORT}
REDIS_HOST: ${REDIS_HOST}
# save (persist) data and also to share data between containers
volumes:
- ./:/var/www/nest-chat-realtime
- /var/www/nest-chat-realtime/node_modules
# application network, each container for a service joins this network
networks:
- nest-chat-network
# Redis service
redis:
# image name
image: redis:latest
# container name
container_name: redis
# execute the command once start redis container
command: [
"redis-server",
"--bind",
"redis",
"--port",
"6379"
]
# save (persist) data and also to share data between containers
volumes:
- red_data:/var/lib/redis
# redis port, this is take value from env file
ports:
- '${REDIS_PORT}:${REDIS_PORT}'
# application network, each container for a service joins this network
networks:
- nest-chat-network

#Docker Networks
networks:
# All container connect in a network
nest-chat-network:
driver: bridge
# save (persist) data
volumes:
red_data: {}

Finally, we can start our application by running docker-compose up .

And the final step is client code, let’s going to create an underlying code with HTML, CSS, and Javascript to show our chat application to clients. I imagine that you are familiar with Vuejs already. So, in this tutorial will use VueJS to initialize the client codes.

As I mentioned above that the client codes are situated in resource folder as figure 1.2 illustrates below.

Figure 1.2: Create HTML, CSS, and JS files in the resource folder

main.js

const app = new Vue({
el: '#app',
data: {
title: 'NestJS Chat Real Time',
name: '',
text: '',
selected: 'general',
messages: [],
socket: null,
activeRoom: '',
rooms: {
general: false,
roomA: false,
roomB: false,
roomC: false,
roomD: false,
},
listRooms: [
"general",
"roomA",
"roomB",
"roomC",
"roomD"
]
},
methods: {
onChange(event) {
this.socket.emit('leaveRoom', this.activeRoom);
this.activeRoom = event.target.value;
this.socket.emit('joinRoom', this.activeRoom);
},

sendMessage() {
if(this.validateInput()) {
const message = {
name: this.name,
text: this.text,
room: this.activeRoom
};
this.socket.emit('msgToServer', message);
this.text = '';
}
},
receivedMessage(message) {
this.messages.push(message)
},
validateInput() {
return this.name.length > 0 && this.text.length > 0
},
check() {
if (this.isMemberOfActiveRoom) {
this.socket.emit('leaveRoom', this.activeRoom);
} else {
this.socket.emit('joinRoom', this.activeRoom);
}
}
},
computed: {
isMemberOfActiveRoom() {
return this.rooms[this.activeRoom];
}
},
created() {
this.activeRoom = this.selected;
this.socket = io('http://localhost:3004/chat');
this.socket.on('msgToClient', (message) => {
console.log(message);
this.receivedMessage(message)
});

this.socket.on('connect', () => {
this.check();
});

this.socket.on('joinedRoom', (room) => {
this.rooms[room] = true;
});

this.socket.on('leftRoom', (room) => {
this.rooms[room] = false;
});
}
});

index.html

<!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/4.0.0-alpha.6/css/bootstrap.min.css" integrity="sha384-rwoIResjU2yc3z8GV/NPeZWAv56rSmLldC3R/AZzGRnGxQQKnKkoFVhFQhNUwEyJ" crossorigin="anonymous">
<title>Nestjs Chat Real Time</title>
<link rel="stylesheet" href="styles.css">
<script src="https://cdn.jsdelivr.net/npm/vue/dist/vue.js"></script>
<script type="text/javascript" src="https://cdn.socket.io/socket.io-1.4.5.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/socket.io/2.3.0/socket.io.dev.js"></script>
</head>
<body>
<div id="app" class="container">
<div class="row">
<div class="col-md-6 offset-md-3 col-sm-12">
<h1 class="text-center">{{ title }}</h1>
<br>
<div id="status"></div>
<div id="chat">
<label for="room">Select room:</label>
<select class="form-control" id="room" v-model="selected" @change="onChange($event)">
<option v-for="room of listRooms" :value="room">{{ room }}</option>
</select>
<br>
<label for="username">Your Name:</label>
<input type="text" v-model="name" id="username" class="form-control" placeholder="Enter name...">
<br>
<label for="messages">Chat:</label>
<div class="card">
<div id="messages" class="card-block">
<ul>
<li v-for="message of messages">{{ message.name }}: {{ message.text }}</li>
</ul>
</div>
</div>
<br>
<label for="textarea">Message:</label>
<textarea id="textarea" class="form-control" v-model="text" placeholder="Enter message..."></textarea>
<br>
<button id="send" class="btn" @click.prevent="sendMessage">Send</button>
</div>
</div>
</div>
</div>

<script src="main.js"></script>
</body>
</html>

style.css

#messages{
height:300px;
overflow-y: scroll;
}

#app {
margin-top: 2rem;
}

I will not explain the coding inside those files that are we created, cause rely on the scopes of this article. On the other aspects of the client-side, you can use the other library as React, Angular, or even Javascript vanilla to start the client codes.

Finally, we can start our application and send the messaging via multiple rooms by running the command docker-compose up . And, we can open our chat application via local address `http://localhost:3004/`.

Let’s have a look at these figures illustrate some scenarios of chat in our realtime application.

Figure 1.3: Those two clients are in a different room and they can’t talk to each other.
Figure 1.4: Those two clients are in the same room and they can send messages together.

Here is the full source code, which is located in the Github: https://github.com/phatvo21/nest-chat-realtime

That’s it. Hope you enjoy this article.

Thanks for reading :)

--

--