JavaScript is required to view this website properly.

Building a Scalable Chat Application with NestJS, MongoDB, Redis, WebSockets, Socket.io, and Docker

2026-02-04 · 4 min read

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

bash
npx @nestjs/cli new chat-app

After setup, create a ChatModule with core files:

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

3.2 MongoDB Setup

Install MongoDB integration:

bash
npm install mongoose @nestjs/mongoose

Example ChatRoom schema:

ts
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:

ts
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:

bash
docker pull redis
npm install redis @nestjs/redis

Example configuration:

ts
// 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

yaml
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.

Want to build something similar?

I’m available for freelance and full‑time roles. Let’s talk.