JavaScript is required to view this website properly.

Anik Malik - BlogBuilding a Scalable Chat Application with NestJS, MongoDB, Redis, WebSockets, Socket.io, and Docker

Title Image

1. Introduction

Real-time messaging has become an essential feature in many modern applications, from social media to business platforms. In this blog, I’ll take you through my journey of building a scalable real-time chat application using NestJS, MongoDB, Redis, WebSockets, Socket.io, and Docker. By combining these technologies, I was able to create a chat platform that is not only efficient and reliable but also scalable. By the end of this post, you’ll see how I tackled real-time messaging and successfully developed a chat app capable of handling multiple users while scaling effortlessly.

2. Project Overview

Features of the Chat Application:

  • One-to-One Messaging: This feature enables users to engage in private conversations, providing a secure and personal space for communication.
  • Group Chats: Users can easily join the same room to participate in group discussions.
  • Real-Time Communication: By utilizing WebSockets and Socket.io, we ensure that messaging happens instantly.
  • Data Storage: All user data, chat messages, and room details are safely stored in MongoDB.
  • Enhanced Performance: We use Redis to cache frequently accessed data, which significantly improves scalability.
  • Containerization: Docker allows us to run both the chat application and Redis in containers, creating an isolated environment that’s easy to replicate.

Tech Stack Overview:

  • NestJS: This powerful framework built on Node.js is perfect for crafting efficient and scalable server-side applications.
  • MongoDB: A NoSQL database designed to store chat room details, messages, and user data effectively.
  • Redis: This technology is used for caching active users and the states of chat rooms, greatly enhancing performance.
  • Socket.io: A library that integrates with WebSockets, facilitating smooth and real-time communication.
  • Docker: This tool ensures that both the app and Redis operate in containers, making it straightforward to scale and deploy.

3. Setting Up the Project

3.1 Initial NestJS Setup

To start building the chat application, I first initialized a new NestJS project:

npx @nestjs/cli new chat-app

After the basic setup, I created a ChatModule to handle all the chat-related features:

The project structure looks like this:

src/
├── chat/
│   ├── chat.module.ts
│   ├── chat.gateway.ts
│   ├── chat.service.ts
│   └── chat.controller.ts
└── app.module.ts

The ChatModule contains the logic for the chat service (ChatService), WebSocket events (ChatGateway), and REST API routes (ChatController).

3.2 MongoDB Setup

In this chat application, the ChatRoom schema is essential for managing chat rooms and participants. I used Mongoose to define the schema, which includes various properties to manage room types, participants, and their statuses.

Here’s the complete ChatRoom schema:

First, install the required packages:

npm install mongoose @nestjs/mongoose

Then, I created the ChatRoomSchema and MessageSchema. Here's an example for the chat rooms:

import { Prop, Schema, SchemaFactory } from '@nestjs/mongoose';
import { Document } from 'mongoose';
import { ParticipantStatus, RoomType } from 'src/shared/schemas/chat.schema';

@Schema({ timestamps: true })
export class ChatRoom extends Document {
  @Prop()
  name: string;

  @Prop({ required: true, enum: RoomType })
  type: RoomType;

  @Prop({ required: true, default: true })
  status: boolean;

  @Prop({ type: String })
  image: string;

  @Prop({
    type: [
      {
        userId: String,
        status: { 
          type: String,
          enum: ParticipantStatus,
          default: ParticipantStatus.REQUEST 
        },
        is_deleted: { type: Boolean, default: false },
      },
    ],
    default: [],
  })
  participants: Array<{
    userId: string;
    status: ParticipantStatus;
    is_deleted: boolean;
  }>;

  @Prop({ type: [String], default: [] })
  admins: string[];

  @Prop({ required: true })
  created_by: string;

  @Prop({ required: true })
  updated_by: string;

  @Prop({ default: false })
  is_deleted: boolean;
}

export const ChatRoomSchema = SchemaFactory.createForClass(ChatRoom);

ChatRoomSchema.pre('validate', function (next) {
  if (this.type === RoomType.GROUP && !this.name) {
    next(new Error('Name must be provided for GROUP room type'));
  } else if (this.type === RoomType.SINGLE && this.name) {
    next(new Error('Name should not be provided for SINGLE room type'));
  } else {
    next();
  }
});

ChatRoomSchema.pre('save', function (next) {
  if (this.type === RoomType.SINGLE) {
    this.image = undefined;
  } else if (this.type === RoomType.GROUP && !this.image) {
    this.image = '';
  }
  next();
});

In this schema:

  • The name property is optional for single rooms but required for group rooms.
  • The type property can either be SINGLE or GROUP, ensuring that the application logic respects these distinctions.
  • The participants field keeps track of each participant’s status (e.g., ACCEPT, REJECT) and whether they have been deleted.
  • Pre-validation hooks are implemented to enforce rules regarding room names based on the room type.
  • The save hook ensures that single rooms do not have an associated image, while group rooms default to an empty string if no image is provided.

4. WebSocket and Socket.io for Real-Time Messaging

The chat application uses WebSockets to establish real-time, bidirectional communication between users. Socket.io is integrated with NestJS to simplify WebSocket management.

I created a ChatGateway to handle all the WebSocket connections:

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

@WebSocketGateway({ cors: true })
export class ChatGateway implements OnGatewayConnection, OnGatewayDisconnect {
  @WebSocketServer() server: Server;

  handleConnection(client: Socket) {
    console.log('User connected:', client.id);
  }

  handleDisconnect(client: Socket) {
    console.log('User disconnected:', client.id);
  }

  @SubscribeMessage('message')
  handleMessage(client: Socket, payload: any) {
    console.log('Message received:', payload);
    // Broadcast message to the room
    this.server.to(payload.room).emit('message', payload);
  }
}

The ChatGateway listens for important events, such as when users connect or disconnect, and when messages are sent. Using the @SubscribeMessage decorator, the ChatGateway listens for a 'message' event and then broadcasts it to everyone in the same chat room.

5. Redis for Caching and Performance Optimization

Redis is a key component in improving the scalability of the chat application. I used Redis to store session information and cache frequently accessed data, such as active users and room states.

First, I installed Redis and its dependencies:

docker pull redis
npm install redis @nestjs/redis

I then configured Redis in ChatGateway and ChatModule for caching:

// chat.module.ts
providers: [
    ChatService,
    ChatGateway,
    {
      provide: 'REDIS_CLIENT',
      useFactory: () => {
        return new Redis({
          host: process.env.REDIS_HOST,
          port: +process.env.REDIS_PORT,
        });
      },
    },
  ],
// chat.gateway.ts
@WebSocketGateway(3002, { cors: true })
export class ChatGateway implements OnGatewayConnection, OnGatewayDisconnect {
  constructor(
    private readonly chatService: ChatService
    private readonly chatAuthGuard: ChatAuthGuard,
    @Inject('REDIS_CLIENT') private readonly redisClient: Redis,
  ) {
    this.redisClient.on('connect', () => {
      this.logger.log('Redis connected successfully');
    });

    this.redisClient.on('error', (error) => {
      this.logger.error(`Redis connection error: ${error.message}`);
    });
  }
//other codes here 
}

Redis allows the app to handle frequent read/write operations more efficiently by caching key data and reducing database load.

6. Docker Setup for NestJS and Redis

Docker plays a crucial role in containerizing both the NestJS chat application and Redis for development and production environments. By using Docker Compose, I ensured that both services run together in separate containers.

Here's the docker-compose.yml file:

version: '3'
services:
  app:
    build: .
    ports:
      - '3000:3000'
    depends_on:
      - redis
  redis:
    image: 'redis:latest'
    ports:
      - '6379:6379'

This configuration ensures that Redis is running as a separate service while the chat app communicates with it on port 6379. I also ensured that my application’s Redis connection URL points to redis as the hostname, matching the service name in Docker Compose.

7. Challenges Faced

Some challenges I faced during the development of this chat application include:

  • WebSocket reconnections: Managing reconnections when users disconnect, ensuring the room state is synchronized.
  • Redis connection issues in Docker: Initially, I faced issues where the app couldn’t connect to Redis within Docker. I resolved this by ensuring the Redis service was correctly named in Docker Compose and updating the Redis connection host in my NestJS app to match the service name (redis).
  • Optimizing performance: Ensuring that frequently accessed data (like room participants and chat messages) are cached in Redis to reduce the load on MongoDB.

8. Lessons Learned

Building this chat application taught me valuable lessons about managing WebSocket connections, scaling with Redis, and containerizing services with Docker. Using Socket.io with NestJS made handling real-time communication straightforward, while Redis proved to be a great tool for improving performance and scalability. Additionally, Docker ensured that the development environment mirrored production, reducing potential deployment issues.

9. Conclusion

This chat application demonstrates how to build and scale real-time communication systems using NestJS, MongoDB, Redis, WebSockets, Socket.io, and Docker. Future improvements could include integrating user authentication, adding features like media sharing, and improving fault tolerance with Redis cluster setups. By following these steps, you can build a scalable, real-time chat platform for your own projects.