Azura Logo
GitHub

Realtime Chat Application

Build a realtime chat application with Azura and WebSockets

In this example, we'll build a realtime chat application using Azura's WebSocket support. This example demonstrates how to create a chat server that allows users to join rooms, send messages, and receive updates in real-time.

Key Features

This example demonstrates the following features of Azura:

WebSocket integration for realtime communication
Room-based chat system
User presence tracking
Message broadcasting
Private messaging
Typing indicators
Message history
JWT authentication for secure connections

Server Setup

Let's start by setting up the WebSocket server with Azura:

typescript
// src/index.ts
import { AzuraServer, createWebSocket } from '@atosjs/azura';
import { JwtUtil } from './utils/jwt.util';
import { ChatService } from './services/chat.service';
import { AuthMiddleware } from './middleware/auth.middleware';

// Create Azura server
const app = new AzuraServer();

// Create WebSocket server
const wss = createWebSocket(app.server);

// Create services
const jwtUtil = new JwtUtil();
const chatService = new ChatService();
const authMiddleware = new AuthMiddleware(jwtUtil);

// Handle WebSocket connections
wss.on('connection', async (socket, req) => {
  try {
    // Authenticate the connection
    const token = new URL(req.url, 'http://localhost').searchParams.get('token');
    
    if (!token) {
      socket.close(4001, 'Authentication required');
      return;
    }
    
    // Verify token
    const user = await authMiddleware.verifyToken(token);
    
    if (!user) {
      socket.close(4001, 'Invalid token');
      return;
    }
    
    // Set user data on socket
    socket.userId = user.id;
    socket.username = user.username;
    
    // Add user to the online users list
    chatService.addUser(user.id, user.username, socket);
    
    console.log(`User connected: ${user.username} (${user.id})`);
    
    // Send welcome message
    socket.send(JSON.stringify({
      type: 'connected',
      data: {
        userId: user.id,
        username: user.username,
        message: `Welcome, ${user.username}!`
      }
    }));
    
    // Broadcast user joined
    chatService.broadcastSystemMessage({
      type: 'userJoined',
      data: {
        userId: user.id,
        username: user.username,
        timestamp: new Date().toISOString()
      }
    });
    
    // Handle messages
    socket.on('message', (data) => {
      try {
        const message = JSON.parse(data.toString());
        handleMessage(socket, message);
      } catch (error) {
        console.error('Error parsing message:', error);
        socket.send(JSON.stringify({
          type: 'error',
          data: { message: 'Invalid message format' }
        }));
      }
    });
    
    // Handle disconnection
    socket.on('close', () => {
      console.log(`User disconnected: ${user.username} (${user.id})`);
      
      // Remove user from online users
      chatService.removeUser(user.id);
      
      // Leave all rooms
      chatService.leaveAllRooms(user.id);
      
      // Broadcast user left
      chatService.broadcastSystemMessage({
        type: 'userLeft',
        data: {
          userId: user.id,
          username: user.username,
          timestamp: new Date().toISOString()
        }
      });
    });
  } catch (error) {
    console.error('WebSocket connection error:', error);
    socket.close(4000, 'Connection error');
  }
});

// Start the server
app.listen(3000);
console.log('Server running at http://localhost:3000');

Message Handling

Now, let's implement the message handling logic:

typescript
// src/services/chat.service.ts
export class ChatService {
  private users = new Map();
  private rooms = new Map();
  private userRooms = new Map();
  
  // User management
  addUser(userId, username, socket) {
    this.users.set(userId, { userId, username, socket });
    this.userRooms.set(userId, new Set());
  }
  
  removeUser(userId) {
    this.users.delete(userId);
    this.userRooms.delete(userId);
  }
  
  getUser(userId) {
    return this.users.get(userId);
  }
  
  getAllUsers() {
    return Array.from(this.users.values()).map(({ userId, username }) => ({ userId, username }));
  }
  
  // Room management
  createRoom(roomId, name, isPrivate = false) {
    if (!this.rooms.has(roomId)) {
      this.rooms.set(roomId, {
        id: roomId,
        name,
        isPrivate,
        users: new Set(),
        messages: []
      });
      return true;
    }
    return false;
  }
  
  joinRoom(userId, roomId) {
    const user = this.users.get(userId);
    const room = this.rooms.get(roomId);
    
    if (!user || !room) return false;
    
    room.users.add(userId);
    this.userRooms.get(userId).add(roomId);
    
    // Notify room members
    this.broadcastToRoom(roomId, {
      type: 'userJoinedRoom',
      data: {
        roomId,
        userId,
        username: user.username,
        timestamp: new Date().toISOString()
      }
    });
    
    return true;
  }
  
  leaveRoom(userId, roomId) {
    const room = this.rooms.get(roomId);
    if (!room) return false;
    
    room.users.delete(userId);
    this.userRooms.get(userId)?.delete(roomId);
    
    // Notify room members
    this.broadcastToRoom(roomId, {
      type: 'userLeftRoom',
      data: {
        roomId,
        userId,
        timestamp: new Date().toISOString()
      }
    });
    
    // Delete room if empty and not the general room
    if (room.users.size === 0 && roomId !== 'general') {
      this.rooms.delete(roomId);
    }
    
    return true;
  }
  
  leaveAllRooms(userId) {
    const userRooms = this.userRooms.get(userId);
    if (!userRooms) return;
    
    for (const roomId of userRooms) {
      this.leaveRoom(userId, roomId);
    }
  }
  
  getRoomUsers(roomId) {
    const room = this.rooms.get(roomId);
    if (!room) return [];
    
    return Array.from(room.users).map(userId => {
      const user = this.users.get(userId);
      return { userId, username: user.username };
    });
  }
  
  getRoomMessages(roomId, limit = 50) {
    const room = this.rooms.get(roomId);
    if (!room) return [];
    
    return room.messages.slice(-limit);
  }
  
  // Message handling
  sendMessage(userId, roomId, content) {
    const user = this.users.get(userId);
    const room = this.rooms.get(roomId);
    
    if (!user || !room) return false;
    
    // Check if user is in the room
    if (!room.users.has(userId)) return false;
    
    const message = {
      id: Date.now().toString(),
      userId,
      username: user.username,
      roomId,
      content,
      timestamp: new Date().toISOString()
    };
    
    // Store message
    room.messages.push(message);
    
    // Broadcast to room
    this.broadcastToRoom(roomId, {
      type: 'message',
      data: message
    });
    
    return true;
  }
  
  sendPrivateMessage(fromUserId, toUserId, content) {
    const fromUser = this.users.get(fromUserId);
    const toUser = this.users.get(toUserId);
    
    if (!fromUser || !toUser) return false;
    
    const message = {
      id: Date.now().toString(),
      fromUserId,
      fromUsername: fromUser.username,
      toUserId,
      toUsername: toUser.username,
      content,
      timestamp: new Date().toISOString()
    };
    
    // Send to recipient
    toUser.socket.send(JSON.stringify({
      type: 'privateMessage',
      data: message
    }));
    
    // Send confirmation to sender
    fromUser.socket.send(JSON.stringify({
      type: 'privateMessageSent',
      data: message
    }));
    
    return true;
  }
  
  sendTypingStatus(userId, roomId, isTyping) {
    const user = this.users.get(userId);
    const room = this.rooms.get(roomId);
    
    if (!user || !room) return false;
    
    // Check if user is in the room
    if (!room.users.has(userId)) return false;
    
    // Broadcast typing status to room
    this.broadcastToRoom(roomId, {
      type: 'typing',
      data: {
        userId,
        username: user.username,
        roomId,
        isTyping,
        timestamp: new Date().toISOString()
      }
    }, userId); // Exclude sender
    
    return true;
  }
  
  // Broadcasting
  broadcastToRoom(roomId, message, excludeUserId = null) {
    const room = this.rooms.get(roomId);
    if (!room) return;
    
    for (const userId of room.users) {
      if (excludeUserId && userId === excludeUserId) continue;
      
      const user = this.users.get(userId);
      if (user && user.socket.readyState === 1) { // WebSocket.OPEN
        user.socket.send(JSON.stringify(message));
      }
    }
  }
  
  broadcastSystemMessage(message) {
    for (const [userId, user] of this.users.entries()) {
      if (user.socket.readyState === 1) { // WebSocket.OPEN
        user.socket.send(JSON.stringify(message));
      }
    }
  }
}

Client Implementation

Here's a simple client implementation using JavaScript:

javascript
// chat-client.js
class ChatClient {
  constructor(url, token) {
    this.url = url;
    this.token = token;
    this.socket = null;
    this.connected = false;
    this.currentRoom = null;
    this.user = null;
    this.eventListeners = {};
  }
  
  connect() {
    return new Promise((resolve, reject) => {
      this.socket = new WebSocket(`${this.url}?token=${this.token}`);
      
      this.socket.onopen = () => {
        console.log('WebSocket connection established');
        this.connected = true;
      };
      
      this.socket.onmessage = (event) => {
        try {
          const message = JSON.parse(event.data);
          this.handleMessage(message);
          
          if (message.type === 'connected') {
            this.user = {
              userId: message.data.userId,
              username: message.data.username
            };
            resolve(this.user);
          }
        } catch (error) {
          console.error('Error parsing message:', error);
        }
      };
      
      this.socket.onerror = (error) => {
        console.error('WebSocket error:', error);
        reject(error);
      };
      
      this.socket.onclose = (event) => {
        console.log(`WebSocket connection closed: ${event.code} ${event.reason}`);
        this.connected = false;
        this.emit('disconnected', { code: event.code, reason: event.reason });
      };
    });
  }
  
  disconnect() {
    if (this.socket) {
      this.socket.close();
    }
  }
  
  // Room methods
  joinRoom(roomId) {
    this.sendMessage({
      type: 'joinRoom',
      data: { roomId }
    });
  }
  
  leaveRoom(roomId) {
    this.sendMessage({
      type: 'leaveRoom',
      data: { roomId }
    });
  }
  
  createRoom(name, isPrivate = false) {
    this.sendMessage({
      type: 'createRoom',
      data: { name, isPrivate }
    });
  }
  
  // Message methods
  sendMessage(roomId, content) {
    this.sendMessage({
      type: 'message',
      data: { roomId, content }
    });
  }
  
  sendPrivateMessage(toUserId, content) {
    this.sendMessage({
      type: 'privateMessage',
      data: { toUserId, content }
    });
  }
  
  sendTypingStatus(roomId, isTyping) {
    this.sendMessage({
      type: 'typing',
      data: { roomId, isTyping }
    });
  }
  
  // Utility methods
  getRoomUsers(roomId) {
    this.sendMessage({
      type: 'getRoomUsers',
      data: { roomId }
    });
  }
  
  getRoomMessages(roomId, limit = 50) {
    this.sendMessage({
      type: 'getRoomMessages',
      data: { roomId, limit }
    });
  }
  
  // Internal methods
  sendMessage(message) {
    if (!this.connected) {
      throw new Error('Not connected');
    }
    
    this.socket.send(JSON.stringify(message));
  }
  
  handleMessage(message) {
    // Emit event for the message type
    this.emit(message.type, message.data);
    
    // Handle specific messages
    switch (message.type) {
      case 'roomJoined':
        this.currentRoom = message.data.roomId;
        break;
      case 'roomLeft':
        if (this.currentRoom === message.data.roomId) {
          this.currentRoom = null;
        }
        break;
    }
  }
  
  // Event handling
  on(event, callback) {
    if (!this.eventListeners[event]) {
      this.eventListeners[event] = [];
    }
    this.eventListeners[event].push(callback);
  }
  
  off(event, callback) {
    if (!this.eventListeners[event]) return;
    
    if (callback) {
      this.eventListeners[event] = this.eventListeners[event].filter(cb => cb !== callback);
    } else {
      delete this.eventListeners[event];
    }
  }
  
  emit(event, data) {
    if (!this.eventListeners[event]) return;
    
    for (const callback of this.eventListeners[event]) {
      callback(data);
    }
  }
}

// Usage example
async function startChat() {
  const client = new ChatClient('ws://localhost:3000/chat', 'your-jwt-token');
  
  try {
    const user = await client.connect();
    console.log(`Connected as ${user.username}`);
    
    // Join the general room
    client.joinRoom('general');
    
    // Listen for messages
    client.on('message', (data) => {
      console.log(`${data.username}: ${data.content}`);
    });
    
    // Listen for private messages
    client.on('privateMessage', (data) => {
      console.log(`[Private] ${data.fromUsername}: ${data.content}`);
    });
    
    // Listen for typing status
    client.on('typing', (data) => {
      console.log(`${data.username} is typing...`);
    });
    
    // Listen for errors
    client.on('error', (data) => {
      console.error('Error:', data.message);
    });
    
    // Send a message to the general room
    client.sendMessage('general', 'Hello, everyone!');
    
    // Send a private message
    client.sendPrivateMessage('user123', 'Hello, this is a private message');
    
    // Send typing status
    client.sendTypingStatus('general', true);
    setTimeout(() => {
      client.sendTypingStatus('general', false);
    }, 3000);
  } catch (error) {
    console.error('Failed to connect:', error);
  }
}

startChat();