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);
});
With Socket.io (Recommended)
// 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
- MDN WebSockets Guide
- Socket.io Documentation
- WebSocket vs SSE vs Polling
- RFC 6455 - WebSocket Protocol
Related Articles
- REST API Design Best Practices
- API Authentication Methods
- GraphQL vs REST vs tRPC
- API Gateway Patterns
Comments