How to Easily Build Real-Time Applications Using NestJS and WebSockets

Hello, World :). I am Martin, a software developer. I am passionate about software development and my favorite stack is React/Next.js, Nest.js and .NET. When I am not coding I love reading books, and articles and exploring the internet (mostly dribbble).
Introduction
Real-time applications allow instant communication and dynamic interaction between users and systems, such as chat apps, live notifications, and collaborative tools. The core idea is to have immediate updates that keep everyone in sync without delays.
WebSockets provide a bidirectional communication protocol to maintain an open connection between client and server, enabling real-time data exchange.
Using NestJS for WebSocket integration simplifies the development of real-time apps by providing structured modules like WebSocket gateways and streamlined event handling. This brings scalability, maintainability, and ease of implementation for developers building high-performance live systems.
To understand more about Nest.js check this article
Importance and Benefits of Using NestJS with WebSockets
Combining NestJS with WebSockets offers several advantages:
Type Safety: TypeScript support ensures robust, error-free code
Scalability: Built-in support for microservices and modular architecture
Developer Experience: Decorators and dependency injection make code clean and maintainable
Real-time Capabilities: Seamless WebSocket integration for instant communication
Testing Support: Built-in testing utilities and mocking capabilities
Note: The following article includes code snippets to illustrate various concepts. Please refer to the explanations and additional information in the official documentation.
Setting Up the NestJS Project
Installing NestJS CLI
npm install -g @nestjs/cli
Generating a New NestJS Project
bashnest new real-time-app
cd real-time-app
The generated project structure:
src/
βββ app.controller.spec.ts
βββ app.controller.ts
βββ app.module.ts
βββ app.service.ts
βββ main.ts
Update main.ts to enable CORS for our React frontend:
// src/main.ts
import { NestFactory } from '@nestjs/core';
import { AppModule } from './app.module';
async function bootstrap() {
const app = await NestFactory.create(AppModule);
app.enableCors({
origin: 'http://localhost:3000', // frontend url
credentials: true,
});
const PORT = 3001;
await app.listen(PORT);
console.log(`API running on http://localhost:${PORT}`);
}
bootstrap();
Installing Necessary WebSockets Dependencies
npm install @nestjs/websockets @nestjs/platform-socket.io socket.io
npm install --save-dev @types/socket.io
Create a WebSocket gateway:
// src/chat/chat.gateway.ts
import {
WebSocketGateway,
SubscribeMessage,
MessageBody,
ConnectedSocket,
OnGatewayInit,
OnGatewayConnection,
OnGatewayDisconnect,
} from '@nestjs/websockets';
import { Socket, Server } from 'socket.io';
import { Logger } from '@nestjs/common';
@WebSocketGateway()
export class ChatGateway implements OnGatewayInit, OnGatewayConnection, OnGatewayDisconnect {
private logger: Logger = new Logger('ChatGateway');
private connectedClients: Map<string, Socket> = new Map();
afterInit(server: Server) {
this.logger.log('WebSocket Gateway initialized');
}
handleConnection(client: Socket) {
this.logger.log(`Client connected: ${client.id}`);
this.connectedClients.set(client.id, client);
// Send welcome message
client.emit('connection-success', {
message: 'Successfully connected to chat server',
clientId: client.id,
});
}
handleDisconnect(client: Socket) {
this.logger.log(`Client disconnected: ${client.id}`);
this.connectedClients.delete(client.id);
}
@SubscribeMessage('send-message')
handleMessage(
@MessageBody() data: { message: string; username: string },
@ConnectedSocket() client: Socket,
): void {
this.logger.log(`Message from ${data.username}: ${data.message}`);
// Broadcast message to all connected clients
client.broadcast.emit('receive-message', {
message: data.message,
username: data.username,
timestamp: new Date().toISOString(),
clientId: client.id,
});
}
@SubscribeMessage('join-room')
handleJoinRoom(
@MessageBody() data: { room: string; username: string },
@ConnectedSocket() client: Socket,
): void {
client.join(data.room);
this.logger.log(`${data.username} joined room: ${data.room}`);
client.to(data.room).emit('user-joined', {
username: data.username,
room: data.room,
timestamp: new Date().toISOString(),
});
}
}
What's happening here:
@WebSocketGateway() marks this class as a WebSocket server that can handle real-time connections from multiple clients simultaneously.
@SubscribeMessage('send-message') tells the server to execute the handleMessage method whenever a client sends a message with the event name 'send-message'.
@MessageBody() extracts the actual data payload from incoming WebSocket messages, similar to how REST APIs extract JSON from request bodies.
@ConnectedSocket() injects the specific client's socket connection into the method, allowing the server to interact with that particular client.
client.broadcast.emit() sends messages to all connected clients except the sender, preventing users from seeing their own messages duplicated.
client.join(data.room) adds the client to a specific chat room using Socket.IO's built-in room management system.
client.to(data.room).emit() sends messages only to clients within a specific room rather than broadcasting to the entire server.
Map<string, Socket> stores all active client connections using their unique IDs as keys, enabling the server to track who's online and manage connections efficiently.
The lifecycle methods (afterInit, handleConnection, handleDisconnect) automatically execute when the server starts, clients connect, or clients disconnect, providing hooks for initialization and cleanup tasks.
Logger provides structured logging for debugging and monitoring chat activity, tracking connections, messages, and room activities throughout the application.
Create a chat module and register the gateway:
// src/chat/chat.module.ts
import { Module } from '@nestjs/common';
import { ChatGateway } from './chat.gateway';
@Module({
providers: [ChatGateway],
})
export class ChatModule {}
providers: [ChatGateway] registers the ChatGateway class with NestJS's dependency injection system, making it available throughout the application.
Update the main app module:
// src/app.module.ts
import { Module } from '@nestjs/common';
import { AppController } from './app.controller';
import { AppService } from './app.service';
import { ChatModule } from './chat/chat.module';
@Module({
imports: [ChatModule],
controllers: [AppController],
providers: [AppService],
})
export class AppModule {}
Broadcasting Messages to Connected Clients
Here's how to implement different broadcasting patterns:
// src/chat/chat.gateway.ts (extended)
export class ChatGateway {
@WebSocketServer()
server: Server;
// Broadcast to all clients
@SubscribeMessage('broadcast-message')
broadcastToAll(@MessageBody() data: any): void {
this.server.emit('global-message', data);
}
// Broadcast to specific room
@SubscribeMessage('room-message')
broadcastToRoom(
@MessageBody() data: { room: string; message: string },
): void {
this.server.to(data.room).emit('room-message', data);
}
// Send to specific client
sendToClient(clientId: string, event: string, data: any): void {
const client = this.connectedClients.get(clientId);
if (client) {
client.emit(event, data);
}
}
}
@WebSocketServer() injects the main Socket.IO server instance, giving access to all connected clients and server-level operations.
this.server.emit() broadcasts messages to every single connected client on the server without any filtering or exclusions.
this.server.to(data.room).emit() sends messages only to clients who have joined a specific room, enabling targeted group communication.
this.connectedClients.get(clientId) retrieves a specific client's socket connection from the stored Map using their unique ID.
client.emit(event, data) sends a message directly to one individual client, enabling private messaging or targeted notifications.
The sendToClient method provides programmatic access to send messages to specific users from other parts of the application, not just in response to incoming WebSocket events.
React.js Frontend Implementation
Create a React component to interact with the WebSocket server:
// src/components/ChatComponent.tsx
import React, { useState, useEffect } from 'react';
import { io, Socket } from 'socket.io-client';
interface Message {
message: string;
username: string;
timestamp: string;
clientId: string;
}
const ChatComponent: React.FC = () => {
const [socket, setSocket] = useState<Socket | null>(null);
const [messages, setMessages] = useState<Message[]>([]);
const [message, setMessage] = useState('');
const [username, setUsername] = useState('');
const [isConnected, setIsConnected] = useState(false);
useEffect(() => {
// Initialize socket connection
const newSocket = io('http://localhost:3001');
setSocket(newSocket);
// Connection event handlers
newSocket.on('connect', () => {
setIsConnected(true);
console.log('Connected to server');
});
newSocket.on('disconnect', () => {
setIsConnected(false);
console.log('Disconnected from server');
});
newSocket.on('connection-success', (data) => {
console.log('Connection success:', data);
});
newSocket.on('receive-message', (data: Message) => {
setMessages(prev => [...prev, data]);
});
// Cleanup on unmount
return () => {
newSocket.close();
};
}, []);
const sendMessage = () => {
if (socket && message.trim() && username.trim()) {
socket.emit('send-message', {
message: message.trim(),
username: username.trim(),
});
// Add own message to chat
setMessages(prev => [...prev, {
message: message.trim(),
username: username.trim(),
timestamp: new Date().toISOString(),
clientId: 'self',
}]);
setMessage('');
}
};
return (
<div className="chat-container">
<div className="connection-status">
Status: {isConnected ? 'π’ Connected' : 'π΄ Disconnected'}
</div>
<div className="user-setup">
<input
type="text"
placeholder="Enter your username"
value={username}
onChange={(e) => setUsername(e.target.value)}
/>
</div>
<div className="messages-container">
{messages.map((msg, index) => (
<div key={index} className="message">
<strong>{msg.username}:</strong> {msg.message}
<small>{new Date(msg.timestamp).toLocaleTimeString()}</small>
</div>
))}
</div>
<div className="message-input">
<input
type="text"
placeholder="Type your message..."
value={message}
onChange={(e) => setMessage(e.target.value)}
onKeyPress={(e) => e.key === 'Enter' && sendMessage()}
/>
<button onClick={sendMessage}>Send</button>
</div>
</div>
);
};
export default ChatComponent;
io('http://localhost:3001') creates a Socket.IO client connection to the WebSocket server running on port 3001.
useState<Socket | null>(null) manages the socket connection instance in React state, allowing the component to track connection status.
useState<Message[]>([]) stores all chat messages in an array that updates when new messages arrive.
useEffect(() => {}, []) runs once when the component mounts to initialize the socket connection and set up event listeners.
newSocket.on('connect') listens for successful connection events from the server and updates the UI connection status.
newSocket.on('receive-message') listens for incoming messages from other users and adds them to the messages array using setMessages(prev => [...prev, data]).
socket.emit('send-message') sends the user's message to the server with the event name that matches the server's @SubscribeMessage('send-message').
setMessages(prev => [...prev, {...}]) adds the user's own message to the chat immediately for instant feedback, since broadcast messages don't include the sender.
onKeyPress={(e) => e.key === 'Enter' && sendMessage()} allows users to send messages by pressing Enter instead of clicking the button.
return () => { newSocket.close(); } cleans up the socket connection when the component unmounts to prevent memory leaks.
Tools and Techniques for Efficient Debugging
Socket.IO Admin UI: Monitor connections and events
Browser DevTools: Network tab for WebSocket frames
Logging: Comprehensive logging for all events
Health Checks: Monitor server performance
// Health check endpoint
@Controller('health')
export class HealthController {
constructor(private chatService: ChatService) {}
@Get('websockets')
getWebSocketHealth() {
return {
activeConnections: this.chatService.getActiveConnections(),
uptime: process.uptime(),
timestamp: new Date().toISOString(),
};
}
}
Best Practices for Building Real-Time Apps
Security Considerations
Authentication: Verify user identity before allowing connections
@UseGuards(WsJwtAuthGuard) @SubscribeMessage('send-message') handleMessage() { // Only authenticated users can send messages }Rate Limiting: Prevent spam and DoS attacks
// Custom rate limiter @Injectable() export class RateLimitGuard implements CanActivate { private requests = new Map(); canActivate(context: ExecutionContext): boolean { const client = context.switchToWs().getClient(); const now = Date.now(); const windowMs = 60000; // 1 minute const maxRequests = 10; const clientRequests = this.requests.get(client.id) || []; const validRequests = clientRequests.filter(time => now - time < windowMs); if (validRequests.length >= maxRequests) { return false; } validRequests.push(now); this.requests.set(client.id, validRequests); return true; } }Input Validation: Sanitize all incoming data
// DTO with validation export class SendMessageDto { @IsString() @Length(1, 500) message: string; @IsString() @Length(1, 50) username: string; }
Conclusion
NestJS combined with WebSockets offers a clean, efficient way to build real-time applications that scale. Its modular architecture and built-in support simplify handling live data like chat, notifications, and updates. Paired with React.js for the frontend, it empowers developers to create dynamic, interactive user experiences with minimal complexity.
Exploring NestJSβs advanced features and best practices unlocks even greater potential for real-time app development. This approach is a solid foundation for modern applications seeking seamless real-time communication.

Let me know in the comments if you want a full code exampleβI'm excited to share it with you!



