Skip to main content
โšก Calmops

Real-time APIs with WebSockets: Complete Guide

Real-time APIs with WebSockets: Complete Guide

Real-time communication is essential for modern applications like chat apps, live notifications, and collaborative tools. This guide covers WebSockets and alternative approaches for building real-time APIs.

What is Real-time Communication?

Real-time communication allows servers to push data to clients instantly, without clients polling for updates.

Use Cases

  • Chat applications
  • Live notifications
  • Collaborative editing (Google Docs style)
  • Real-time dashboards
  • Gaming
  • Stock price updates
  • IoT device updates

WebSockets Overview

WebSockets provide full-duplex communication over a single TCP connection.

How WebSockets Work

โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”
โ”‚                    WebSocket Connection                      โ”‚
โ”œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ค
โ”‚                                                             โ”‚
โ”‚  Client                                                  โ”‚
โ”‚     โ”‚                                                      โ”‚
โ”‚     โ”‚  1. HTTP Upgrade Request                            โ”‚
โ”‚     โ”‚โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€>โ”‚
โ”‚     โ”‚                                                      โ”‚
โ”‚     โ”‚  2. 101 Switching Protocols                         โ”‚
โ”‚     โ”‚<โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”‚
โ”‚     โ”‚                                                      โ”‚
โ”‚     โ”‚  3. Bi-directional WebSocket Frames                  โ”‚
โ”‚     โ”‚<โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€>โ”‚
โ”‚     โ”‚                                                      โ”‚
โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜

WebSocket Handshake

# Client Request
GET /ws/chat HTTP/1.1
Host: api.example.com
Upgrade: websocket
Connection: Upgrade
Sec-WebSocket-Key: dGhlIHNhbXBsZSBub25jZQ==
Sec-WebSocket-Version: 13

# Server Response
HTTP/1.1 101 Switching Protocols
Upgrade: websocket
Connection: Upgrade
Sec-WebSocket-Accept: s3pPLMBiTxaQ9kYGzzhZRbK+xOo=

WebSocket Implementation

Server Implementation (Node.js)

const { WebSocketServer } = require('ws');

const wss = new WebSocketServer({ port: 8080 });

// Handle connections
wss.on('connection', (ws, req) => {
  const clientIp = req.socket.remoteAddress;
  console.log(`Client connected: ${clientIp}`);
  
  // Send welcome message
  ws.send(JSON.stringify({
    type: 'welcome',
    message: 'Connected to WebSocket server'
  }));
  
  // Handle messages
  ws.on('message', (message) => {
    const data = JSON.parse(message);
    
    switch (data.type) {
      case 'chat':
        // Broadcast to all clients
        broadcast({
          type: 'chat',
          message: data.message,
          timestamp: Date.now()
        });
        break;
        
      case 'ping':
        ws.send(JSON.stringify({ type: 'pong' }));
        break;
    }
  });
  
  // Handle close
  ws.on('close', () => {
    console.log('Client disconnected');
  });
  
  // Handle errors
  ws.on('error', (error) => {
    console.error('WebSocket error:', error);
  });
});

function broadcast(message) {
  wss.clients.forEach((client) => {
    if (client.readyState === WebSocket.OPEN) {
      client.send(JSON.stringify(message));
    }
  });
}

Client Implementation

// Browser client
class WebSocketClient {
  constructor(url) {
    this.url = url;
    this.ws = null;
    this.listeners = new Map();
  }
  
  connect() {
    return new Promise((resolve, reject) => {
      this.ws = new WebSocket(this.url);
      
      this.ws.onopen = () => {
        console.log('Connected');
        resolve();
      };
      
      this.ws.onmessage = (event) => {
        const data = JSON.parse(event.data);
        this.emit(data.type, data);
      };
      
      this.ws.onerror = (error) => {
        console.error('WebSocket error:', error);
        reject(error);
      };
      
      this.ws.onclose = () => {
        console.log('Disconnected');
        this.emit('disconnect', {});
      };
    });
  }
  
  send(type, payload) {
    if (this.ws.readyState === WebSocket.OPEN) {
      this.ws.send(JSON.stringify({ type, ...payload }));
    }
  }
  
  on(event, callback) {
    if (!this.listeners.has(event)) {
      this.listeners.set(event, []);
    }
    this.listeners.get(event).push(callback);
  }
  
  emit(event, data) {
    const callbacks = this.listeners.get(event) || [];
    callbacks.forEach(cb => cb(data));
  }
}

// Usage
const client = new WebSocketClient('wss://api.example.com/ws');

client.connect().then(() => {
  client.send('chat', { message: 'Hello!' });
});

client.on('chat', (data) => {
  console.log('New message:', data.message);
});
// Server
const { Server } = require('socket.io');

const io = new Server(3000, {
  cors: {
    origin: ['https://yourapp.com'],
    methods: ['GET', 'POST']
  }
});

io.on('connection', (socket) => {
  console.log(`User connected: ${socket.id}`);
  
  // Join room
  socket.on('join-room', (roomId) => {
    socket.join(roomId);
    socket.to(roomId).emit('user-joined', socket.id);
  });
  
  // Send message to room
  socket.on('chat-message', ({ roomId, message }) => {
    io.to(roomId).emit('new-message', {
      id: Date.now(),
      message,
      sender: socket.id,
      timestamp: new Date().toISOString()
    });
  });
  
  // Typing indicator
  socket.on('typing', ({ roomId }) => {
    socket.to(roomId).emit('user-typing', socket.id);
  });
  
  // Disconnect
  socket.on('disconnect', () => {
    console.log('User disconnected');
  });
});

// Client
const io = require('socket.io-client');
const socket = io('https://api.example.com');

socket.on('connect', () => {
  console.log('Connected:', socket.id);
});

socket.emit('join-room', 'room-123');

socket.on('new-message', (message) => {
  console.log('New message:', message);
});

Connection Management

Heartbeat/Ping-Pong

const HEARTBEAT_INTERVAL = 30000; // 30 seconds

wss.on('connection', (ws) => {
  let isAlive = true;
  
  // Heartbeat timer
  const heartbeat = setInterval(() => {
    if (ws.readyState === WebSocket.OPEN) {
      ws.ping();
    }
  }, HEARTBEAT_INTERVAL);
  
  ws.on('pong', () => {
    isAlive = true;
  });
  
  ws.on('close', () => {
    clearInterval(heartbeat);
  });
  
  // Check connection on message
  ws.isAlive = true;
  ws.on('message', () => {
    ws.isAlive = true;
  });
});

// Close dead connections
setInterval(() => {
  wss.clients.forEach((ws) => {
    if (ws.isAlive === false) {
      return ws.terminate();
    }
    ws.isAlive = false;
    ws.ping();
  });
}, 60000);

Authentication

// Token-based authentication
wss.on('connection', (ws, req) => {
  const url = new URL(req.url, 'ws://localhost');
  const token = url.searchParams.get('token');
  
  if (!token) {
    ws.close(4001, 'Authentication required');
    return;
  }
  
  try {
    const user = jwt.verify(token, process.env.JWT_SECRET);
    ws.user = user;
    ws.send(JSON.stringify({ type: 'authenticated', user: user.id }));
  } catch (error) {
    ws.close(4002, 'Invalid token');
  }
});

// Client sends token
const ws = new WebSocket('wss://api.example.com/ws?token=' + token);

Reconnection Logic

class ReconnectingWebSocket {
  constructor(url, options = {}) {
    this.url = url;
    this.reconnectInterval = options.reconnectInterval || 1000;
    this.maxReconnectAttempts = options.maxReconnectAttempts || 5;
    this.reconnectAttempts = 0;
    this.connect();
  }
  
  connect() {
    this.ws = new WebSocket(this.url);
    
    this.ws.onclose = () => {
      if (this.reconnectAttempts < this.maxReconnectAttempts) {
        this.reconnectAttempts++;
        const delay = this.reconnectInterval * Math.pow(2, this.reconnectAttempts - 1);
        console.log(`Reconnecting in ${delay}ms...`);
        setTimeout(() => this.connect(), delay);
      }
    };
    
    this.ws.onopen = () => {
      this.reconnectAttempts = 0;
      console.log('Connected');
    };
  }
  
  send(data) {
    if (this.ws.readyState === WebSocket.OPEN) {
      this.ws.send(data);
    }
  }
}

Scaling WebSockets

With Redis Adapter (Socket.io)

const { Server } = require('socket.io');
const { createAdapter } = require('@socket.io/redis-adapter');
const { createClient } = require('redis');

const pubClient = createClient({ url: 'redis://localhost:6379' });
const subClient = pubClient.duplicate();

const io = new Server({
  adapter: createAdapter(pubClient, subClient)
});

With Multiple Servers

                    โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”
                    โ”‚   Load Balancer โ”‚
                    โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ฌโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜
                             โ”‚
        โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ผโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”
        โ”‚                    โ”‚                    โ”‚
        โ–ผ                    โ–ผ                    โ–ผ
โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”  โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”  โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”
โ”‚  WebSocket    โ”‚  โ”‚  WebSocket   โ”‚  โ”‚  WebSocket   โ”‚
โ”‚  Server 1     โ”‚  โ”‚  Server 2    โ”‚  โ”‚  Server 3    โ”‚
โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ฌโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜  โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ฌโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜  โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ฌโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜
        โ”‚                    โ”‚                    โ”‚
        โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ผโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜
                             โ”‚
                    โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ดโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”
                    โ”‚     Redis       โ”‚
                    โ”‚   (Pub/Sub)    โ”‚
                    โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜

Sticky Sessions

// nginx.conf
upstream websocket {
  server ws1.example.com:8080;
  server ws2.example.com:8080;
  server ws3.example.com:8080;
}

server {
  location /ws/ {
    proxy_pass http://websocket;
    proxy_http_version 1.1;
    proxy_set_header Upgrade $http_upgrade;
    proxy_set_header Connection "upgrade";
    proxy_read_timeout 86400;
  }
}

Fallback Strategies

Server-Sent Events (SSE)

SSE is simpler than WebSockets and works over HTTP:

// Server (Express)
app.get('/events', (req, res) => {
  res.setHeader('Content-Type', 'text/event-stream');
  res.setHeader('Cache-Control', 'no-cache');
  res.setHeader('Connection', 'keep-alive');
  
  // Send initial data
  res.write(`data: ${JSON.stringify({ type: 'connected' })}\n\n`);
  
  // Send updates
  const interval = setInterval(() => {
    res.write(`data: ${JSON.stringify({ time: Date.now() })}\n\n`);
  }, 1000);
  
  res.on('close', () => {
    clearInterval(interval);
  });
});
// Client
const eventSource = new EventSource('/events');

eventSource.onmessage = (event) => {
  const data = JSON.parse(event.data);
  console.log('Received:', data);
};

eventSource.onerror = () => {
  console.log('Connection lost');
};

Long Polling

// Server
app.get('/poll', async (req, res) => {
  const lastUpdate = parseInt(req.query.lastUpdate) || 0;
  
  // Wait for new data
  const data = await waitForUpdate(lastUpdate, 30000); // 30s timeout
  
  res.json(data);
});

function waitForUpdate(since, timeout) {
  return new Promise((resolve) => {
    const check = () => {
      const updates = getUpdatesSince(since);
      if (updates.length > 0) {
        resolve({ updates });
      } else {
        setTimeout(check, 1000);
      }
    };
    
    setTimeout(() => resolve({ updates: [] }), timeout);
    check();
  });
}

// Client
async function longPoll() {
  const response = await fetch(`/poll?lastUpdate=${lastUpdate}`);
  const data = await response.json();
  
  data.updates.forEach(update => processUpdate(update));
  lastUpdate = Date.now();
  
  longPoll(); // Recursively poll
}

Comparison

Feature WebSockets SSE Long Polling
Browser Support Modern Modern All
Bi-directional Yes Serverโ†’Client Yes
Connections 1 1 Many
Firewall Friendly No (WS) Yes Yes
Complexity Medium Low High
Automatic Reconnect Manual Built-in Manual

Security Considerations

WSS (WebSocket Secure)

// Always use TLS
const wss = new WebSocketServer({
  port: 8080,
  // Require secure connection
});

// Or with https
const server = https.createServer(credentials);
const wss = new WebSocketServer({ server });

Origin Validation

const wss = new WebSocketServer({
  port: 8080,
  verifyClient: (info) => {
    const allowedOrigins = ['https://yourapp.com', 'https://app.yourapp.com'];
    const origin = info.origin;
    
    if (!allowedOrigins.includes(origin)) {
      return false; // Reject connection
    }
    return true;
  }
});

Message Size Limits

const wss = new WebSocketServer({
  port: 8080,
  maxPayload: 1024 * 1024 // 1MB limit
});

Input Validation

ws.on('message', (message) => {
  try {
    const data = JSON.parse(message);
    
    // Validate message structure
    if (!data.type || typeof data.payload !== 'object') {
      ws.send(JSON.stringify({
        type: 'error',
        message: 'Invalid message format'
      }));
      return;
    }
    
    // Sanitize string inputs
    if (typeof data.payload.message === 'string') {
      data.payload.message = sanitize(data.payload.message);
    }
    
    processMessage(data);
  } catch (error) {
    ws.send(JSON.stringify({
      type: 'error',
      message: 'Invalid JSON'
    }));
  }
});

Best Practices

Do’s

  • Use WSS (WebSocket Secure) in production
  • Implement heartbeat/ping-pong
  • Handle reconnection gracefully
  • Validate all incoming messages
  • Use message versioning for protocol changes
  • Scale with Redis pub/sub for multiple servers

Don’ts

  • Don’t send sensitive data without encryption
  • Don’t trust client messages without validation
  • Don’t store WebSocket connections in memory if scaling horizontally
  • Don’t forget to clean up connections on disconnect

External Resources


Comments