Build a Real-Time Chat App with NestJS, WebSocket (Socket.IO) & MongoDB

by Didin J. on Aug 09, 2025 Build a Real-Time Chat App with NestJS, WebSocket (Socket.IO) & MongoDB

Learn to build a realtime chat app with NestJS, Socket.IO WebSockets and MongoDB (Mongoose). Step-by-step guide with backend, frontend, and Docker.

In this tutorial, you will learn how to build a real-time chat application using NestJS, Socket.IO, and MongoDB.

By the end of this guide, you’ll have a functional chat system where multiple clients can join chat rooms, exchange messages instantly, and retrieve chat history from MongoDB.

This is perfect for building live chat systems, collaboration tools, online games, or real-time dashboards.

Prerequisites

Before you start, make sure you have the following installed:

  • Node.js 18+ and npm/yarn

  • Nest CLI (optional but recommended) → npm install -g @nestjs/cli

  • Docker (optional for running MongoDB locally)

  • Basic knowledge of TypeScript and NestJS

Technologies Used

  • NestJS — a progressive Node.js framework for building scalable server-side applications

  • Socket.IO — real-time bidirectional event-based communication

  • MongoDB — NoSQL database for storing chat messages

  • Mongoose — MongoDB object modeling for Node.js

  • dotenv — for environment configuration


1. Setting Up the NestJS Project

First, create a new NestJS application:

nest new realtime-chat-nestjs
cd realtime-chat-nestjs

Install the required dependencies:

npm install @nestjs/mongoose mongoose @nestjs/websockets @nestjs/platform-socket.io socket.io
npm install @nestjs/config


2. Configure Environment Variables

Create a .env file in the project root:

MONGO_URI=mongodb://localhost:27017/nest-chat
PORT=3000


3. Enable CORS in main.ts

Open src/main.ts and update it:

import { NestFactory } from '@nestjs/core';
import { AppModule } from './app.module';

async function bootstrap() {
  const app = await NestFactory.create(AppModule);
  app.enableCors({ origin: '*' });
  await app.listen(process.env.PORT || 3000);
}
bootstrap();


4. Connect to MongoDB in app.module.ts

import { Module } from '@nestjs/common';
import { AppController } from './app.controller';
import { AppService } from './app.service';
import { ConfigModule } from '@nestjs/config';
import { MongooseModule } from '@nestjs/mongoose';
import { ChatModule } from './chat/chat.module';

@Module({
  imports: [
    ConfigModule.forRoot(),
    MongooseModule.forRoot(process.env.MONGO_URI!),
    ChatModule,
  ],
  controllers: [AppController],
  providers: [AppService],
})
export class AppModule { }


5. Create the Chat Module

nest generate module chat
nest generate service chat
nest generate gateway chat
nest generate controller chat


6. Define the Message Schema

src/chat/schemas/message.schema.ts:

import { Prop, Schema, SchemaFactory } from '@nestjs/mongoose';
import { Document } from 'mongoose';

export type MessageDocument = Message & Document;

@Schema({ timestamps: true })
export class Message {
    @Prop({ required: true })
    username: string;

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

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

    @Prop()
    createdAt?: Date;

    @Prop()
    updatedAt?: Date;
}

export const MessageSchema = SchemaFactory.createForClass(Message);


7. Create DTO for Messages

src/chat/dto/create-message.dto.ts:

export class CreateMessageDto {
  username: string;
  room: string;
  content: string;
}


8. Implement the Chat Service

src/chat/chat.service.ts:

import { Injectable } from '@nestjs/common';
import { InjectModel } from '@nestjs/mongoose';
import { Model } from 'mongoose';
import { CreateMessageDto } from './dto/create-message.dto';
import { Message, MessageDocument } from './schemas/message.schema';

@Injectable()
export class ChatService {
    constructor(@InjectModel(Message.name) private messageModel: Model<MessageDocument>) { }

    async create(createMessageDto: CreateMessageDto) {
        const created = new this.messageModel(createMessageDto);
        return created.save();
    }

    async findLastMessages(room: string, limit = 50) {
        return this.messageModel
            .find({ room })
            .sort({ createdAt: -1 })
            .limit(limit)
            .lean()
            .exec();
    }
}


9. Implement the WebSocket Gateway

src/chat/chat.gateway.ts:

import {
  WebSocketGateway,
  WebSocketServer,
  SubscribeMessage,
  OnGatewayInit,
  OnGatewayConnection,
  OnGatewayDisconnect,
  MessageBody,
  ConnectedSocket,
} from '@nestjs/websockets';
import { Server, Socket } from 'socket.io';
import { ChatService } from './chat.service';
import { CreateMessageDto } from './dto/create-message.dto';

@WebSocketGateway({ cors: { origin: '*' } })
export class ChatGateway implements OnGatewayInit, OnGatewayConnection, OnGatewayDisconnect {
  @WebSocketServer() server: Server;

  constructor(private readonly chatService: ChatService) {}

  afterInit() {
    console.log('WebSocket server initialized');
  }

  handleConnection(client: Socket) {
    console.log(`Client connected: ${client.id}`);
  }

  handleDisconnect(client: Socket) {
    console.log(`Client disconnected: ${client.id}`);
  }

  @SubscribeMessage('joinRoom')
  handleJoinRoom(@MessageBody() payload: { room: string }, @ConnectedSocket() client: Socket) {
    client.join(payload.room);
    client.emit('joined', { room: payload.room });
  }

  @SubscribeMessage('leaveRoom')
  handleLeaveRoom(@MessageBody() payload: { room: string }, @ConnectedSocket() client: Socket) {
    client.leave(payload.room);
    client.emit('left', { room: payload.room });
  }

  @SubscribeMessage('message')
  async handleMessage(@MessageBody() payload: CreateMessageDto) {
    const saved = await this.chatService.create(payload);

    this.server.to(payload.room).emit('message', {
      _id: saved._id,
      username: saved.username,
      content: saved.content,
      room: saved.room,
      createdAt: saved.createdAt,
    });
  }
}


10. Create the REST Controller for Chat History

src/chat/chat.controller.ts:

import { Controller, Get, Query } from '@nestjs/common';
import { ChatService } from './chat.service';

@Controller('chat')
export class ChatController {
  constructor(private readonly chatService: ChatService) {}

  @Get('history')
  async history(@Query('room') room: string, @Query('limit') limit?: string) {
    const l = limit ? parseInt(limit, 10) : 50;
    const msgs = await this.chatService.findLastMessages(room, l);
    return msgs.reverse();
  }
}


11. Register the Chat Module

src/chat/chat.module.ts:

import { Module } from '@nestjs/common';
import { MongooseModule } from '@nestjs/mongoose';
import { ChatService } from './chat.service';
import { ChatGateway } from './chat.gateway';
import { Message, MessageSchema } from './schemas/message.schema';
import { ChatController } from './chat.controller';

@Module({
  imports: [MongooseModule.forFeature([{ name: Message.name, schema: MessageSchema }])],
  providers: [ChatService, ChatGateway],
  controllers: [ChatController],
})
export class ChatModule {}


12. Create a Simple Frontend

Create index.html in the project root or public/ folder:

<!doctype html>
<html>
<head>
  <meta charset="utf-8" />
  <title>NestJS Chat Demo</title>
  <style>
    body { font-family: Arial; padding: 1rem; }
    #messages { border: 1px solid #ddd; height: 300px; overflow: auto; padding: 0.5rem; }
    .msg { margin-bottom: 0.5rem; }
  </style>
</head>
<body>
  <h2>Realtime Chat</h2>
  <label>Username: <input id="username" value="guest" /></label>
  <label>Room: <input id="room" value="general" /></label>
  <button id="join">Join</button>
  <button id="leave">Leave</button>
  <div id="messages"></div>
  <form id="form">
    <input id="input" autocomplete="off" /><button>Send</button>
  </form>

  <script src="https://cdn.socket.io/4.6.1/socket.io.min.js"></script>
  <script>
    const socket = io('http://localhost:3000');
    const messages = document.getElementById('messages');
    const usernameInput = document.getElementById('username');
    const roomInput = document.getElementById('room');
    let currentRoom = roomInput.value;

    document.getElementById('join').onclick = () => {
      currentRoom = roomInput.value;
      socket.emit('joinRoom', { room: currentRoom });
      fetchHistory(currentRoom);
    };

    document.getElementById('leave').onclick = () => {
      socket.emit('leaveRoom', { room: currentRoom });
    };

    socket.on('message', (msg) => addMessage(msg));

    document.getElementById('form').onsubmit = (e) => {
      e.preventDefault();
      const content = document.getElementById('input').value.trim();
      if (!content) return;
      socket.emit('message', {
        username: usernameInput.value || 'guest',
        room: currentRoom,
        content
      });
      document.getElementById('input').value = '';
    };

    async function fetchHistory(room) {
      const res = await fetch(`http://localhost:3000/chat/history?room=${room}&limit=50`);
      const data = await res.json();
      messages.innerHTML = '';
      data.forEach(addMessage);
    }

    function addMessage(msg) {
      const el = document.createElement('div');
      el.className = 'msg';
      el.innerHTML = `<strong>${msg.username}</strong>: ${msg.content} <small>${new Date(msg.createdAt).toLocaleTimeString()}</small>`;
      messages.appendChild(el);
      messages.scrollTop = messages.scrollHeight;
    }
  </script>
</body>
</html>


13. Running the Application

Start MongoDB (local or Docker):

docker run --name nest-mongo -p 27017:27017 -d mongo:6

Start the NestJS backend:

npm run start:dev

Open index.html in your browser (I use Chrome and Safari) and start chatting in real time.

Build a Real-Time Chat App with NestJS, WebSocket (Socket.IO) & MongoDB - chat app


14. Production & Security Notes

  • Authentication: Use JWT to secure connections and enable private chat rooms.

  • Validation: Add class-validator to validate incoming messages.

  • Scaling: Use a Redis adapter for Socket.IO to handle multiple server instances.

  • Sanitization: Escape HTML to prevent XSS attacks.


Conclusion

You’ve now built a real-time chat application using NestJS, Socket.IO, and MongoDB.
This app demonstrates the core concepts of handling WebSocket events, persisting chat history, and building a simple frontend to display messages.

With this foundation, you can easily add authentication, private rooms, file sharing, and more.

You can get the full source code on our GitHub.

That's just the basics. If you need more deep learning about Nest.js, you can take the following cheap course:

Thanks!