Skip to content

Building Real-time Applications with WebSockets

Building Real-time Applications with WebSockets: A Comprehensive Guide

Real-time applications are becoming increasingly important in modern web development. WebSockets provide a powerful way to implement real-time features. This guide will show you how to build various types of real-time applications.

Understanding WebSockets

WebSockets provide full-duplex communication channels over a single TCP connection:

// @filename: index.js
// Browser-side WebSocket
const ws = new WebSocket('ws://localhost:8080')

ws.onopen = () => {
  console.log('Connected to WebSocket server')
}

ws.onmessage = (event) => {
  console.log('Received:', event.data)
}

ws.onclose = () => {
  console.log('Disconnected from WebSocket server')
}

ws.onerror = (error) => {
  console.error('WebSocket error:', error)
}

Setting Up a WebSocket Server

Let’s create a WebSocket server using Node.js and ws library:

// @filename: index.js
// server.js
const WebSocket = require('ws')
const server = new WebSocket.Server({ port: 8080 })

server.on('connection', (ws) => {
  console.log('New client connected')

  ws.on('message', (message) => {
    console.log('Received:', message)

    // Broadcast to all clients
    server.clients.forEach((client) => {
      if (client !== ws && client.readyState === WebSocket.OPEN) {
        client.send(message)
      }
    })
  })

  ws.on('close', () => {
    console.log('Client disconnected')
  })
})

Project: Real-time Chat Application

Let’s build a complete chat application with rooms and private messaging:

// @filename: main.py
// types.ts
interface ChatMessage {
  id: string
  type: 'message' | 'system'
  room: string
  sender: string
  content: string
  timestamp: number
}

interface ChatRoom {
  id: string
  name: string
  users: Set<string>
}

// server/ChatServer.ts

class ChatServer {
  private wss: WebSocketServer
  private rooms: Map<string, ChatRoom>
  private clients: Map<WebSocket, string> // WebSocket -> userId

  constructor(port: number) {
    this.wss = new WebSocketServer({ port })
    this.rooms = new Map()
    this.clients = new Map()

    // Create default room
    this.rooms.set('general', {
      id: 'general',
      name: 'General',
      users: new Set()
    })

    this.setupWebSocketServer()
  }

  private setupWebSocketServer() {
    this.wss.on('connection', (ws: WebSocket) => {
      const userId = uuidv4()
      this.clients.set(ws, userId)

      // Join default room
      this.joinRoom(ws, 'general')

      ws.on('message', (data: string) => {
        const message = JSON.parse(data)
        this.handleMessage(ws, message)
      })

      ws.on('close', () => {
        this.handleDisconnect(ws)
      })
    })
  }

  private handleMessage(ws: WebSocket, message: any) {
    const userId = this.clients.get(ws)

    switch (message.type) {
      case 'chat':
        this.broadcastToRoom(message.room, {
          id: uuidv4(),
          type: 'message',
          room: message.room,
          sender: userId,
          content: message.content,
          timestamp: Date.now()
        })
        break

      case 'join_room':
        this.joinRoom(ws, message.room)
        break

      case 'leave_room':
        this.leaveRoom(ws, message.room)
        break

      case 'private_message':
        this.sendPrivateMessage(
          userId!,
          message.recipient,
          message.content
        )
        break
    }
  }

  private broadcastToRoom(roomId: string, message: ChatMessage) {
    const room = this.rooms.get(roomId)
    if (!room) return

    this.wss.clients.forEach((client) => {
      if (
        client.readyState === WebSocket.OPEN &&
        room.users.has(this.clients.get(client)!)
      ) {
        client.send(JSON.stringify(message))
      }
    })
  }

  private joinRoom(ws: WebSocket, roomId: string) {
    const userId = this.clients.get(ws)
    const room = this.rooms.get(roomId)

    if (!room || !userId) return

    room.users.add(userId)

    // Notify room about new user
    this.broadcastToRoom(roomId, {
      id: uuidv4(),
      type: 'system',
      room: roomId,
      sender: 'system',
      content: `User ${userId} joined the room`,
      timestamp: Date.now()
    })
  }

  private leaveRoom(ws: WebSocket, roomId: string) {
    const userId = this.clients.get(ws)
    const room = this.rooms.get(roomId)

    if (!room || !userId) return

    room.users.delete(userId)

    // Notify room about user leaving
    this.broadcastToRoom(roomId, {
      id: uuidv4(),
      type: 'system',
      room: roomId,
      sender: 'system',
      content: `User ${userId} left the room`,
      timestamp: Date.now()
    })
  }

  private sendPrivateMessage(
    senderId: string,
    recipientId: string,
    content: string
  ) {
    const message = {
      id: uuidv4(),
      type: 'private_message',
      sender: senderId,
      content,
      timestamp: Date.now()
    }

    this.wss.clients.forEach((client) => {
      const clientId = this.clients.get(client)
      if (
        client.readyState === WebSocket.OPEN &&
        clientId === recipientId
      ) {
        client.send(JSON.stringify(message))
      }
    })
  }

  private handleDisconnect(ws: WebSocket) {
    const userId = this.clients.get(ws)
    if (!userId) return

    // Remove user from all rooms
    this.rooms.forEach((room) => {
      if (room.users.has(userId)) {
        room.users.delete(userId)
        this.broadcastToRoom(room.id, {
          id: uuidv4(),
          type: 'system',
          room: room.id,
          sender: 'system',
          content: `User ${userId} disconnected`,
          timestamp: Date.now()
        })
      }
    })

    this.clients.delete(ws)
  }
}

// client/ChatClient.ts
class ChatClient {
  private ws: WebSocket
  private messageHandlers: Map<string, (message: any) => void>

  constructor(url: string) {
    this.ws = new WebSocket(url)
    this.messageHandlers = new Map()

    this.ws.onmessage = (event) => {
      const message = JSON.parse(event.data)
      this.handleMessage(message)
    }
  }

  public sendMessage(room: string, content: string) {
    this.ws.send(JSON.stringify({
      type: 'chat',
      room,
      content
    }))
  }

  public joinRoom(room: string) {
    this.ws.send(JSON.stringify({
      type: 'join_room',
      room
    }))
  }

  public leaveRoom(room: string) {
    this.ws.send(JSON.stringify({
      type: 'leave_room',
      room
    }))
  }

  public sendPrivateMessage(recipient: string, content: string) {
    this.ws.send(JSON.stringify({
      type: 'private_message',
      recipient,
      content
    }))
  }

  public onMessage(type: string, handler: (message: any) => void) {
    this.messageHandlers.set(type, handler)
  }

  private handleMessage(message: any) {
    const handler = this.messageHandlers.get(message.type)
    if (handler) {
      handler(message)
    }
  }
}

// React component example


function ChatRoom({ roomId }: { roomId: string }) {
  const [messages, setMessages] = useState<ChatMessage[]>([])
  const [input, setInput] = useState('')
  const [client, setClient] = useState<ChatClient | null>(null)

  useEffect(() => {
    const chatClient = new ChatClient('ws://localhost:8080')
    setClient(chatClient)

    chatClient.onMessage('message', (message) => {
      setMessages((prev) => [...prev, message])
    })

    chatClient.onMessage('system', (message) => {
      setMessages((prev) => [...prev, message])
    })

    chatClient.joinRoom(roomId)

    return () => {
      chatClient.leaveRoom(roomId)
    }
  }, [roomId])

  const sendMessage = () => {
    if (input.trim() && client) {
      client.sendMessage(roomId, input)
      setInput('')
    }
  }

  return (
    <div className="chat-room">
      <div className="messages">
        {messages.map((message) => (
          <div
            key={message.id}
            className={`message ${message.type}`}
          >
            <span className="sender">{message.sender}</span>
            <span className="content">{message.content}</span>
            <span className="time">
              {new Date(message.timestamp).toLocaleTimeString()}
            </span>
          </div>
        ))}
      </div>

      <div className="input-area">
        <input
          type="text"
          value={input}
          onChange={(e) => setInput(e.target.value)}
          onKeyPress={(e) => e.key === 'Enter' && sendMessage()}
        />
        <button onClick={sendMessage}>Send</button>
      </div>
    </div>
  )
}

Real-time Dashboard Example

Let’s create a real-time dashboard that updates automatically:

// @filename: index.ts
// server/DashboardServer.ts
class DashboardServer {
  private wss: WebSocketServer
  private metrics: Map<string, number>
  private updateInterval: NodeJS.Timer

  constructor(port: number) {
    this.wss = new WebSocketServer({ port })
    this.metrics = new Map()

    this.setupWebSocketServer()
    this.startMetricsUpdate()
  }

  private setupWebSocketServer() {
    this.wss.on('connection', (ws: WebSocket) => {
      // Send initial metrics
      ws.send(JSON.stringify({
        type: 'metrics',
        data: Object.fromEntries(this.metrics)
      }))
    })
  }

  private startMetricsUpdate() {
    this.updateInterval = setInterval(() => {
      // Update metrics
      this.metrics.set('cpu', Math.random() * 100)
      this.metrics.set('memory', Math.random() * 16384)
      this.metrics.set('requests', Math.floor(Math.random() * 1000))

      // Broadcast to all clients
      this.broadcastMetrics()
    }, 1000)
  }

  private broadcastMetrics() {
    const message = JSON.stringify({
      type: 'metrics',
      data: Object.fromEntries(this.metrics)
    })

    this.wss.clients.forEach((client) => {
      if (client.readyState === WebSocket.OPEN) {
        client.send(message)
      }
    })
  }
}

// React dashboard component
function Dashboard() {
  const [metrics, setMetrics] = useState({
    cpu: 0,
    memory: 0,
    requests: 0
  })

  useEffect(() => {
    const ws = new WebSocket('ws://localhost:8080')

    ws.onmessage = (event) => {
      const message = JSON.parse(event.data)
      if (message.type === 'metrics') {
        setMetrics(message.data)
      }
    }

    return () => ws.close()
  }, [])

  return (
    <div className="dashboard">
      <div className="metric">
        <h3>CPU Usage</h3>
        <div className="value">{metrics.cpu.toFixed(1)}%</div>
      </div>

      <div className="metric">
        <h3>Memory Usage</h3>
        <div className="value">
          {(metrics.memory / 1024).toFixed(2)} GB
        </div>
      </div>

      <div className="metric">
        <h3>Requests/sec</h3>
        <div className="value">{metrics.requests}</div>
      </div>
    </div>
  )
}

Best Practices

  1. Connection Management

    • Implement reconnection logic
    • Handle connection errors
    • Clean up resources properly
    • Monitor connection health
  2. Performance

    • Minimize message size
    • Batch updates when possible
    • Use binary protocols for large data
    • Implement rate limiting
  3. Security

    • Use WSS (WebSocket Secure)
    • Validate messages
    • Implement authentication
    • Prevent DoS attacks
  4. Scalability

    • Use Redis for pub/sub
    • Implement horizontal scaling
    • Monitor performance
    • Handle backpressure

Common WebSocket Use Cases

  1. Real-time Collaboration
// @filename: index.ts
interface CursorPosition {
  userId: string
  x: number
  y: number
}

// Broadcast cursor positions
ws.send(
  JSON.stringify({
    type: 'cursor_move',
    position: { x, y },
  })
)
  1. Live Updates
// @filename: index.ts
// Subscribe to updates
ws.send(
  JSON.stringify({
    type: 'subscribe',
    topics: ['prices', 'inventory'],
  })
)
  1. Multiplayer Games
// @filename: index.ts
interface GameState {
  players: Map<string, PlayerState>
  gameObjects: GameObject[]
}

// Send player action
ws.send(
  JSON.stringify({
    type: 'player_action',
    action: 'move',
    direction: { x: 1, y: 0 },
  })
)

Conclusion

WebSockets enable powerful real-time features in web applications:

  • Bi-directional communication
  • Low latency updates
  • Efficient resource usage
  • Scalable architecture

Keep exploring different use cases and implementing best practices for robust real-time applications.


Further Reading

WebSocket Real-time Communication Scalability
Share:

Continue Reading

Building a REST API with Node.js and MongoDB: A Step-by-Step Guide

Building a REST API is essential for modern web applications, enabling you to interact with your backend using standard HTTP methods. In this guide, we will build a REST API with Node.js, Express, and MongoDB, covering everything from setting up the project to implementing CRUD operations. By the end, you will have a fully functional API that you can use as a backend for web or mobile applications.

Read article
Node.jsMongoDBREST API

Implementing Efficient Caching in Node.js with node-cache

Boost Node.js performance by up to 80% with efficient caching using node-cache. Complete guide with setup, TTL configuration, memory management, and production best practices. Includes code examples and performance benchmarks.

Read article
Node.jsJavaScriptBackend

AI-Assisted Content

This article includes AI-assisted content that has been reviewed for accuracy. Always test code snippets before use.