Real-time messaging has become a core feature in modern products. In this post, I’ll walk through how I built a scalable real‑time chat application using NestJS, MongoDB, Redis, WebSockets, Socket.io, and Docker. By the end, you’ll understand the architecture, key implementation details, and how each piece helps the system scale.
1. Introduction
Real‑time messaging powers products across social, collaboration, and enterprise workflows. The goal here is to build a chat app that is reliable, fast, and easy to scale.
2. Project Overview
Features
- One‑to‑One Messaging for private conversations
- Group Chats with room‑based messaging
- Real‑Time Communication via WebSockets and Socket.io
- Persistent Storage in MongoDB
- Caching with Redis for better performance
- Containerization using Docker for repeatable environments
Tech Stack
- NestJS for a structured, scalable backend
- MongoDB for chat rooms, messages, and user data
- Redis for caching and fast access to active state
- Socket.io for real‑time messaging
- Docker for containerized services
3. Setting Up the Project
3.1 Initial NestJS Setup
npx @nestjs/cli new chat-appAfter setup, create a ChatModule with core files:
src/
├── chat/
│ ├── chat.module.ts
│ ├── chat.gateway.ts
│ ├── chat.service.ts
│ └── chat.controller.ts
└── app.module.ts3.2 MongoDB Setup
Install MongoDB integration:
npm install mongoose @nestjs/mongooseExample ChatRoom schema:
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();
});4. WebSocket and Socket.io
NestJS makes real‑time messaging clean with gateways:
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) {
this.server.to(payload.room).emit('message', payload);
}
}5. Redis for Caching
Redis helps reduce database load and keeps active state in memory.
Install:
docker pull redis
npm install redis @nestjs/redisExample configuration:
// 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}`);
});
}
}6. Docker Setup
version: '3'
services:
app:
build: .
ports:
- '3000:3000'
depends_on:
- redis
redis:
image: 'redis:latest'
ports:
- '6379:6379'The app connects to Redis using the service name redis.
7. Challenges Faced
- WebSocket reconnections and resyncing room state
- Redis connection issues in Docker resolved by using the service name
- Performance tuning by caching hot data in Redis
8. Lessons Learned
Socket.io + NestJS simplifies real‑time features. Redis is essential for scale, and Docker keeps environments consistent from dev to production.
9. Conclusion
This architecture makes real‑time chat reliable, fast, and scalable. If I continue the project, I’d add authentication, media sharing, and Redis clustering for high availability.