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.
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).
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 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.
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.
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.
Some challenges I faced during the development of this chat application include:
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.
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.