Building a Real-Time Chat Application with NestJS and WebSockets
Introduction
Real-time communication has become essential in modern web applications. Whether you're building a customer support system, collaborative tools, or social platforms, implementing WebSocket-based real-time features is crucial. In this guide, we'll build a robust chat application using NestJS and its powerful WebSocket capabilities.
Setting Up the NestJS WebSocket Gateway
First, let's install the necessary dependencies:
npm install @nestjs/websockets @nestjs/platform-socket.io socket.ioCreate a chat gateway that will handle WebSocket connections:
import {
WebSocketGateway,
WebSocketServer,
SubscribeMessage,
ConnectedSocket,
MessageBody,
OnGatewayConnection,
OnGatewayDisconnect,
} from '@nestjs/websockets';
import { Server, Socket } from 'socket.io';
import { Logger } from '@nestjs/common';
@WebSocketGateway({
cors: {
origin: '*',
},
})
export class ChatGateway implements OnGatewayConnection, OnGatewayDisconnect {
@WebSocketServer()
server: Server;
private logger: Logger = new Logger('ChatGateway');
private connectedUsers = new Map();
handleConnection(client: Socket) {
this.logger.log(`Client connected: ${client.id}`);
}
handleDisconnect(client: Socket) {
const user = this.connectedUsers.get(client.id);
if (user) {
this.server.emit('userLeft', {
userId: user.userId,
username: user.username,
timestamp: new Date(),
});
this.connectedUsers.delete(client.id);
}
this.logger.log(`Client disconnected: ${client.id}`);
}
@SubscribeMessage('joinRoom')
handleJoinRoom(
@ConnectedSocket() client: Socket,
@MessageBody() data: { roomId: string; userId: string; username: string }
) {
client.join(data.roomId);
this.connectedUsers.set(client.id, {
userId: data.userId,
username: data.username,
});
client.to(data.roomId).emit('userJoined', {
userId: data.userId,
username: data.username,
timestamp: new Date(),
});
this.logger.log(`User ${data.username} joined room ${data.roomId}`);
}
@SubscribeMessage('sendMessage')
handleMessage(
@ConnectedSocket() client: Socket,
@MessageBody() data: { roomId: string; message: string; userId: string; username: string }
) {
const messageData = {
id: Math.random().toString(36).substr(2, 9),
message: data.message,
userId: data.userId,
username: data.username,
timestamp: new Date(),
};
this.server.to(data.roomId).emit('newMessage', messageData);
this.logger.log(`Message sent to room ${data.roomId}: ${data.message}`);
}
} Creating a Chat Service
Let's create a service to handle chat-related business logic and database operations:
import { Injectable } from '@nestjs/common';
export interface ChatRoom {
id: string;
name: string;
createdAt: Date;
}
export interface ChatMessage {
id: string;
roomId: string;
userId: string;
username: string;
message: string;
timestamp: Date;
}
@Injectable()
export class ChatService {
private rooms = new Map();
private messages = new Map();
createRoom(name: string): ChatRoom {
const room: ChatRoom = {
id: Math.random().toString(36).substr(2, 9),
name,
createdAt: new Date(),
};
this.rooms.set(room.id, room);
this.messages.set(room.id, []);
return room;
}
getRooms(): ChatRoom[] {
return Array.from(this.rooms.values());
}
getRoom(roomId: string): ChatRoom | undefined {
return this.rooms.get(roomId);
}
getRoomMessages(roomId: string): ChatMessage[] {
return this.messages.get(roomId) || [];
}
saveMessage(message: ChatMessage): void {
const roomMessages = this.messages.get(message.roomId) || [];
roomMessages.push(message);
this.messages.set(message.roomId, roomMessages);
}
} Building REST API Endpoints
Create a controller to handle HTTP requests for chat rooms and message history:
import { Controller, Get, Post, Body, Param } from '@nestjs/common';
import { ChatService, ChatRoom, ChatMessage } from './chat.service';
@Controller('chat')
export class ChatController {
constructor(private readonly chatService: ChatService) {}
@Post('rooms')
createRoom(@Body() createRoomDto: { name: string }): ChatRoom {
return this.chatService.createRoom(createRoomDto.name);
}
@Get('rooms')
getRooms(): ChatRoom[] {
return this.chatService.getRooms();
}
@Get('rooms/:roomId')
getRoom(@Param('roomId') roomId: string): ChatRoom {
return this.chatService.getRoom(roomId);
}
@Get('rooms/:roomId/messages')
getRoomMessages(@Param('roomId') roomId: string): ChatMessage[] {
return this.chatService.getRoomMessages(roomId);
}
}Module Configuration
Wire everything together in your chat module:
import { Module } from '@nestjs/common';
import { ChatGateway } from './chat.gateway';
import { ChatController } from './chat.controller';
import { ChatService } from './chat.service';
@Module({
controllers: [ChatController],
providers: [ChatGateway, ChatService],
})
export class ChatModule {}Advanced Features
Message Validation and Sanitization
Implement proper validation for incoming messages:
import { IsString, IsNotEmpty, MaxLength } from 'class-validator';
export class SendMessageDto {
@IsString()
@IsNotEmpty()
roomId: string;
@IsString()
@IsNotEmpty()
@MaxLength(500)
message: string;
@IsString()
@IsNotEmpty()
userId: string;
@IsString()
@IsNotEmpty()
username: string;
}Rate Limiting
Add rate limiting to prevent spam:
import { UseGuards } from '@nestjs/common';
import { ThrottlerGuard } from '@nestjs/throttler';
@UseGuards(ThrottlerGuard)
@SubscribeMessage('sendMessage')
handleMessage(
@ConnectedSocket() client: Socket,
@MessageBody() data: SendMessageDto
) {
// Message handling logic
}Frontend Integration
Here's how to connect from a React frontend:
import io from 'socket.io-client';
const socket = io('http://localhost:3000');
// Join a room
socket.emit('joinRoom', {
roomId: 'room-123',
userId: 'user-456',
username: 'John Doe'
});
// Send message
socket.emit('sendMessage', {
roomId: 'room-123',
message: 'Hello everyone!',
userId: 'user-456',
username: 'John Doe'
});
// Listen for new messages
socket.on('newMessage', (message) => {
console.log('New message:', message);
});Conclusion
This implementation provides a solid foundation for real-time chat functionality using NestJS and WebSockets. The modular architecture makes it easy to extend with features like message persistence, user authentication, and file sharing. Remember to implement proper error handling, authentication, and database integration for production use.
Related Posts
Mastering Laravel Queues: A Complete Guide to Background Job Processing
Learn how to implement and optimize Laravel queues for better application performance and user experience.
Building Scalable GraphQL APIs with DataLoader in Node.js
Learn how to eliminate the N+1 query problem in GraphQL using Facebook's DataLoader pattern for efficient data fetching.
Building Scalable Node.js APIs with Express and TypeScript: A Production-Ready Setup
Learn to build robust, type-safe Node.js APIs using Express and TypeScript with proper error handling, validation, and project structure.