Real time chat application
  • Backend Development
  • Nestjs
  • nodejs
  • Real-Time Chat Application
  • WebSocket

Building a Real-Time Chat Application with NestJS and WebSocket

In this comprehensive guide, we’ll explore how to create a real-time chat application using NestJS and WebSocket. We’ll cover essential features like user authentication, sending…

   

In this comprehensive guide, we’ll explore how to create a real-time chat application using NestJS and WebSocket. We’ll cover essential features like user authentication, sending messages, and displaying typing status. This application will provide a seamless and interactive user experience.

Prerequisites

Before we begin, ensure you have the following installed:

  • Node.js and npm: Necessary for running the backend server and managing dependencies.
  • Basic understanding of NestJS and TypeScript: Familiarity with these technologies will help you follow along with the implementation.
  • Knowledge of WebSocket: Understanding the basics of WebSocket will be beneficial for implementing real-time communication.

Setting Up the Project

  1. Create a NestJS Project
    • Initialize a new NestJS project.
nest new chat-app

Once the project is created, navigate to the project directory.

Next, install the necessary dependencies for WebSocket and JWT authentication

Creating the Chat Module

Generate a new module and gateway for handling WebSocket connections. This will structure our code in a modular way, following the best practices encouraged by NestJS.

npm install @nestjs/websockets @nestjs/platform-socket.io socket.io jsonwebtoken redis dotenv

Implementing the Chat Gateway

The ChatGateway will handle WebSocket connections, user authentication, message broadcasting, and typing status. Let’s break down the implementation step by step.

Authenticating Users

In chat.gateway.ts, handle user connections and verify JWT tokens to ensure authenticated access:

async handleConnection(client: Socket) {
  const authorizationHeader = client.handshake.headers.authorization;
  const token = authorizationHeader;
  let isValidToken = false;
  if (authorizationHeader) {
    try {
      const decodedToken: any = verify(token, process.env.JWT_SECRET);
      if (decodedToken && (decodedToken.exp * 1000 > Date.now())) {
        isValidToken = true;
        this.activeUsers.forEach((value, key) => {
          if (value.userId === decodedToken.id) {
            this.activeUsers.delete(key);
          }
        });
        this.activeUsers.set(client.id, { userId: decodedToken.id, roomId: null });
        this.broadcastUsers();
      }
    } catch (error) {
      console.error('Error verifying token:', error);
    }
  }

  client.emit('tokenVerification', isValidToken);

  if (!isValidToken) {
    client.disconnect(true);
  }
}

Explanation: Verifies the JWT token from the client’s headers and adds the user to the active users list if the token is valid.

Broadcasting Active Users

private broadcastUsers() {
  const uniqueUsers = new Map();
  this.activeUsers.forEach(value => {
    uniqueUsers.set(value.userId, { userId: value.userId, roomId: value.roomId, status: 'online' });
  });
  this.server.emit('update_user_list', Array.from(uniqueUsers.values()));
}

Explanation: Broadcasts the list of active users to all connected clients.

Handling Disconnection

async handleDisconnect(client: Socket) {
  if (this.activeUsers.has(client.id)) {
    this.activeUsers.delete(client.id);
    this.broadcastUsers();
    console.log(`User with client ID ${client.id} disconnected.`);
  }
}

Explanation: Removes the user from the active users list upon disconnection and updates the user list.

Joining a Room

@SubscribeMessage('join_room')
async handleJoinRoom(@MessageBody() data: { room_id: string, user_id: string }, @ConnectedSocket() client: Socket) {
  this.activeUsers.forEach((value, key) => {
    if (value.userId === data.user_id) {
      value.roomId = data.room_id;
      this.activeUsers.set(key, value);
    }
  });

  const userInfo = Array.from(this.activeUsers.values()).find(user => user.userId === data.user_id);
  if (userInfo) {
    client.join(data.room_id);
    this.broadcastUsers();
  }
  console.log(`User with client ID ${client.id} joined room ${data.room_id}.`);
}

Explanation: Allows a user to join a specific chat room and updates the room ID for that user.

Handling Typing Status

@SubscribeMessage('start_typing')
async handleStartTyping(@MessageBody() data: { room_id: string, user_id: string }, @ConnectedSocket() client: Socket) {
this.typingUsers.set(client.id, { userId: data.user_id, roomId: data.room_id });
  this.broadcastTypingStatus();
}

@SubscribeMessage('stop_typing')
async handleStopTyping(@MessageBody() data: { room_id: string, user_id: string }, @ConnectedSocket() client: Socket) {
  if (this.typingUsers.has(client.id)) {
    this.typingUsers.delete(client.id);
    console.log(`User with ID ${data.user_id} has stopped typing in room ${data.room_id}`);
  }
  this.broadcastTypingStatus();
}

private broadcastTypingStatus() {
  const uniqueUsers = new Map();
  this.typingUsers.forEach(value => {
    uniqueUsers.set(value.userId, { userId: value.userId, roomId: value.roomId });
  });
  this.server.emit('typing_status', Array.from(uniqueUsers.values()));
}

Explanation: Manages and broadcasts the typing status of users in real-time.

Sending Messages

@SubscribeMessage('send_message')
async handleMessage(@MessageBody() data: { room_id: string; message: string; sender_id: string; receiver_id: string; product_id: string; module_slug: string; hasBlockedYou: boolean }): Promise<void> {
  const userInfo = Array.from(this.activeUsers.values()).find((user) => user.userId !== data.sender_id && user.roomId === data.room_id);
  const currentDate = new Date();
  const socketMessage = {
    room_id: data.room_id,
    sender_id: data.sender_id,
    message: data.message,
    receiver_id: data.receiver_id,
    module_slug: data.module_slug,
    hasBlockedYou: data.hasBlockedYou,
    is_seen: userInfo ? true : false,
    created_at: currentDate,
  };
  this.server.emit('receive_message', socketMessage);
  const { userData, conversation } = await this.chatService.create({ ...data, is_seen: userInfo ? true : false });
  }
}

Explanation: Users can send messages, which are broadcasted to all clients in the room. The server also manages message persistence and push notifications.

Handling User Actions

New User:
@SubscribeMessage('new_user')
async handleNewUser(@MessageBody() data: { room_id: string, message: string, product_uuid: string, sender_id: string, sender_username: string, sender_avatar: string, product_id: string, product_name: string, product_media: string, module_slug: string, module_id: string, receiver_id: string }): Promise<void> {
  const {
    room_id,
    message,
    sender_id,
    sender_username,
    sender_avatar,
    product_id,
    product_name,
    product_media,
    module_slug,
    product_uuid,
    module_id,
    receiver_id
  } = data;


  const blockedUser = await this.chatService.getUserBlock(receiver_id, sender_id);
  if (blockedUser) {
    console.log(`User ${sender_id} is blocked by ${receiver_id}. Event not emitted.`);
  } else {
    this.server.emit('new_user',data);
  }
}

Blocking and Unblocking Users

Blocking Users:

@SubscribeMessage('unblock_user')
async handleUnblockUser(@MessageBody() data: any): Promise<void> {
  this.server.emit('unblock_user', data);
}

Explanation: Handles the event when a user is blocked by broadcasting the block event to all clients.

Unblocking Users:

@SubscribeMessage('unblock_user')
async handleUnblockUser(@MessageBody() data: any): Promise<void> {
  this.server.emit('unblock_user', data);
}

Explanation: Handles the event when a user is unblocked by broadcasting the unblock event to all clients.

Explanation of the Complete Chat Gateway Code

Initialization and Connection Handling

  • initializeRedis: This function sets up the Redis client connection using credentials from environment variables to manage active connections and store user data efficiently.
  • handleConnection: Verifies JWT tokens from client headers to authenticate users. If valid, it adds users to the active users list and broadcasts the updated user list to all connected clients.
  • handleDisconnect: When a user disconnects, this function removes the user from the active users list and broadcasts the updated list to all clients.

Broadcasting and Managing Users

  • broadcastUsers: This function sends the list of active users to all connected clients, ensuring everyone has the latest information on who is online.
  • broadcastTypingStatus: Broadcasts the typing status of users to all connected clients, indicating when a user is typing in a chat.
  • handleJoinRoom: Allows users to join specific chat rooms and updates the room ID for each user, ensuring they are correctly associated with the room they joined.
  • handleStartTyping: Adds users to the typing list when they start typing and broadcasts their typing status.
  • handleStopTyping: Removes users from the typing list when they stop typing and broadcasts the updated typing status.

Handling Messages and Typing Status

  • handleMessage: Manages sending messages within chat rooms. It broadcasts messages to all clients in the room, saves the message in the database, and handles push notifications for offline users.
  • handleNewUser: Handles events when a new user joins a chat. It broadcasts the new user event to all clients, considering any blocked users to prevent unwanted interactions.
  • handleBlockUser: Manages user blocking events by broadcasting the block event to all clients, ensuring blocked users can’t interact with those who have blocked them.
  • handleUnblockUser: Handles events when a user is unblocked, broadcasting the unblock event to all clients, allowing interactions to resume.
  • handleProductSold: Broadcasts a ‘product sold’ event to all clients, useful in e-commerce scenarios to update users on product availability.

Retrieving Active Users

  • getActiveUsers: Retrieves the list of active users from Redis, providing a real-time view of who is online.

Conclusion

In this tutorial, we’ve built a robust real-time chat application using NestJS and WebSocket. The application includes essential features like user authentication, message broadcasting, typing status indication, and user blocking/unblocking. By following this guide, you can extend the application with additional features like private messaging, message history, and user presence indicators. The combination of NestJS for the backend and WebSocket for real-time communication offers a scalable and maintainable solution for real-time chat applications.

For further assistance or inquiries, Contact Us.

Future Enhancements

Here are some Future enancement of the real time chat application:

  • Private Messaging: Implement private messaging between users for one-on-one conversations.
  • Message History: Store and retrieve message history, allowing users to see past conversations.
  • User Presence Indicators: Show online/offline status for users, providing better visibility of user availability.
  • Push Notifications: Enhance push notification features to notify users of different events, even when they are offline.