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.
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:
-
Learn NestJS from Scratch
-
Mastering NestJS
-
Angular & NestJS Full Stack Web Development Bootcamp 2023
-
React and NestJS: A Practical Guide with Docker
-
NestJS Masterclass - NodeJS Framework Backend Development
-
Build A TodoList with NestJS and Vue JS
-
The Complete Angular & NestJS Course
Thanks!