Skip to main content

Command Palette

Search for a command to run...

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

Updated
β€’8 min read
How to Easily Build Real-Time Applications Using NestJS and WebSockets
M

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

  1. Socket.IO Admin UI: Monitor connections and events

  2. Browser DevTools: Network tab for WebSocket frames

  3. Logging: Comprehensive logging for all events

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

  1. Authentication: Verify user identity before allowing connections

     @UseGuards(WsJwtAuthGuard)
     @SubscribeMessage('send-message')
     handleMessage() {
       // Only authenticated users can send messages
     }
    
  2. 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;
       }
     }
    
  3. 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.

Moving On Mic Drop GIF by VeeFriends

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

More from this blog

The Reactive Dev πŸ‘¨β€πŸ’»πŸˆΊ

15 posts

Hello, World :). I am Martin, a software developer. I am passionate about web development and my favorite stack is Next.js/React.js and Node.js. I do also love .NET ✌️