JavaScript 7 min read

WebSockets: Build Real-Time Applications with Node.js

Master WebSocket development for real-time apps. Learn Socket.IO, authentication, scaling, error handling, and build chat and notification systems.

MR

Moshiour Rahman

Advertisement

What are WebSockets?

WebSockets provide a persistent, full-duplex communication channel between client and server. Unlike HTTP, WebSockets allow servers to push data to clients instantly without polling.

WebSockets vs HTTP

HTTPWebSocket
Request-ResponseBidirectional
StatelessStateful
New connection each requestPersistent connection
High latencyLow latency
Polling for updatesPush updates

Getting Started

Native WebSocket Server

import { WebSocketServer, WebSocket } from 'ws';

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

wss.on('connection', (ws: WebSocket) => {
  console.log('Client connected');

  ws.on('message', (data: Buffer) => {
    const message = data.toString();
    console.log('Received:', message);

    // Echo back
    ws.send(`Server received: ${message}`);
  });

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

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

  // Send welcome message
  ws.send('Welcome to the WebSocket server!');
});

console.log('WebSocket server running on port 8080');

Native WebSocket Client

const ws = new WebSocket('ws://localhost:8080');

ws.onopen = () => {
  console.log('Connected to server');
  ws.send('Hello, server!');
};

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

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

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

Socket.IO

Server Setup

import express from 'express';
import { createServer } from 'http';
import { Server } from 'socket.io';

const app = express();
const httpServer = createServer(app);
const io = new Server(httpServer, {
  cors: {
    origin: 'http://localhost:3000',
    methods: ['GET', 'POST']
  },
  pingInterval: 10000,
  pingTimeout: 5000
});

io.on('connection', (socket) => {
  console.log(`User connected: ${socket.id}`);

  // Handle events
  socket.on('chat:message', (data) => {
    console.log('Message:', data);
    // Broadcast to all clients
    io.emit('chat:message', {
      id: Date.now(),
      userId: socket.id,
      text: data.text,
      timestamp: new Date()
    });
  });

  // Handle disconnect
  socket.on('disconnect', (reason) => {
    console.log(`User disconnected: ${socket.id}, reason: ${reason}`);
  });
});

httpServer.listen(3001, () => {
  console.log('Server running on port 3001');
});

Client Setup

import { io, Socket } from 'socket.io-client';

const socket: Socket = io('http://localhost:3001', {
  autoConnect: true,
  reconnection: true,
  reconnectionAttempts: 5,
  reconnectionDelay: 1000
});

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

socket.on('disconnect', (reason) => {
  console.log('Disconnected:', reason);
});

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

// Send message
function sendMessage(text: string) {
  socket.emit('chat:message', { text });
}

// React Hook
function useSocket() {
  const [isConnected, setIsConnected] = useState(false);
  const [messages, setMessages] = useState<Message[]>([]);

  useEffect(() => {
    socket.on('connect', () => setIsConnected(true));
    socket.on('disconnect', () => setIsConnected(false));
    socket.on('chat:message', (msg) => {
      setMessages((prev) => [...prev, msg]);
    });

    return () => {
      socket.off('connect');
      socket.off('disconnect');
      socket.off('chat:message');
    };
  }, []);

  return { isConnected, messages, sendMessage };
}

Rooms and Namespaces

Namespaces

// Server
const chatNamespace = io.of('/chat');
const notificationNamespace = io.of('/notifications');

chatNamespace.on('connection', (socket) => {
  console.log('User connected to chat');

  socket.on('message', (data) => {
    chatNamespace.emit('message', data);
  });
});

notificationNamespace.on('connection', (socket) => {
  console.log('User connected to notifications');

  socket.on('subscribe', (userId) => {
    socket.join(`user:${userId}`);
  });
});

// Send notification to specific user
function notifyUser(userId: string, notification: Notification) {
  notificationNamespace.to(`user:${userId}`).emit('notification', notification);
}

// Client
const chatSocket = io('http://localhost:3001/chat');
const notificationSocket = io('http://localhost:3001/notifications');

Rooms

// Server - Join room
socket.on('room:join', (roomId: string) => {
  socket.join(roomId);
  socket.to(roomId).emit('user:joined', {
    userId: socket.id,
    roomId
  });
  console.log(`${socket.id} joined room ${roomId}`);
});

// Leave room
socket.on('room:leave', (roomId: string) => {
  socket.leave(roomId);
  socket.to(roomId).emit('user:left', {
    userId: socket.id,
    roomId
  });
});

// Send to room
socket.on('room:message', ({ roomId, text }) => {
  io.to(roomId).emit('room:message', {
    userId: socket.id,
    text,
    roomId,
    timestamp: new Date()
  });
});

// Get users in room
function getUsersInRoom(roomId: string) {
  const room = io.sockets.adapter.rooms.get(roomId);
  return room ? Array.from(room) : [];
}

Authentication

JWT Authentication

import jwt from 'jsonwebtoken';

// Server middleware
io.use((socket, next) => {
  const token = socket.handshake.auth.token;

  if (!token) {
    return next(new Error('Authentication required'));
  }

  try {
    const decoded = jwt.verify(token, process.env.JWT_SECRET!) as { userId: string };
    socket.data.userId = decoded.userId;
    next();
  } catch (error) {
    next(new Error('Invalid token'));
  }
});

io.on('connection', (socket) => {
  console.log(`User ${socket.data.userId} connected`);

  // Join user's personal room
  socket.join(`user:${socket.data.userId}`);
});

// Client
const socket = io('http://localhost:3001', {
  auth: {
    token: localStorage.getItem('token')
  }
});

socket.on('connect_error', (error) => {
  if (error.message === 'Invalid token') {
    // Redirect to login
    window.location.href = '/login';
  }
});

Session Authentication

import session from 'express-session';
import { Server } from 'socket.io';

const sessionMiddleware = session({
  secret: 'your-secret',
  resave: false,
  saveUninitialized: false
});

app.use(sessionMiddleware);

// Share session with Socket.IO
io.engine.use(sessionMiddleware);

io.on('connection', (socket) => {
  const session = socket.request.session;

  if (!session.userId) {
    socket.disconnect();
    return;
  }

  console.log(`User ${session.userId} connected`);
});

Real-Time Chat Application

Server Implementation

interface User {
  id: string;
  name: string;
  socketId: string;
}

interface Message {
  id: string;
  userId: string;
  userName: string;
  text: string;
  roomId: string;
  timestamp: Date;
}

const users = new Map<string, User>();
const rooms = new Map<string, Set<string>>();

io.on('connection', (socket) => {
  let currentUser: User | null = null;

  socket.on('user:login', (name: string) => {
    currentUser = {
      id: socket.data.userId || socket.id,
      name,
      socketId: socket.id
    };
    users.set(currentUser.id, currentUser);

    socket.emit('user:logged', currentUser);
    io.emit('users:online', Array.from(users.values()));
  });

  socket.on('room:join', (roomId: string) => {
    if (!currentUser) return;

    socket.join(roomId);

    if (!rooms.has(roomId)) {
      rooms.set(roomId, new Set());
    }
    rooms.get(roomId)!.add(currentUser.id);

    socket.to(roomId).emit('room:userJoined', {
      user: currentUser,
      roomId
    });

    // Send room members
    const members = Array.from(rooms.get(roomId)!)
      .map((id) => users.get(id))
      .filter(Boolean);
    socket.emit('room:members', { roomId, members });
  });

  socket.on('message:send', ({ roomId, text }) => {
    if (!currentUser) return;

    const message: Message = {
      id: crypto.randomUUID(),
      userId: currentUser.id,
      userName: currentUser.name,
      text,
      roomId,
      timestamp: new Date()
    };

    io.to(roomId).emit('message:new', message);
  });

  socket.on('typing:start', (roomId: string) => {
    if (!currentUser) return;
    socket.to(roomId).emit('typing:user', {
      userId: currentUser.id,
      userName: currentUser.name,
      isTyping: true
    });
  });

  socket.on('typing:stop', (roomId: string) => {
    if (!currentUser) return;
    socket.to(roomId).emit('typing:user', {
      userId: currentUser.id,
      userName: currentUser.name,
      isTyping: false
    });
  });

  socket.on('disconnect', () => {
    if (!currentUser) return;

    users.delete(currentUser.id);
    rooms.forEach((members, roomId) => {
      if (members.has(currentUser!.id)) {
        members.delete(currentUser!.id);
        socket.to(roomId).emit('room:userLeft', {
          userId: currentUser!.id,
          roomId
        });
      }
    });

    io.emit('users:online', Array.from(users.values()));
  });
});

React Chat Component

import { useEffect, useState, useCallback } from 'react';
import { io, Socket } from 'socket.io-client';

const socket: Socket = io('http://localhost:3001');

function ChatRoom({ roomId }: { roomId: string }) {
  const [messages, setMessages] = useState<Message[]>([]);
  const [input, setInput] = useState('');
  const [typing, setTyping] = useState<string[]>([]);

  useEffect(() => {
    socket.emit('room:join', roomId);

    socket.on('message:new', (message: Message) => {
      setMessages((prev) => [...prev, message]);
    });

    socket.on('typing:user', ({ userName, isTyping }) => {
      setTyping((prev) =>
        isTyping
          ? [...prev.filter((n) => n !== userName), userName]
          : prev.filter((n) => n !== userName)
      );
    });

    return () => {
      socket.emit('room:leave', roomId);
      socket.off('message:new');
      socket.off('typing:user');
    };
  }, [roomId]);

  const sendMessage = useCallback(() => {
    if (!input.trim()) return;
    socket.emit('message:send', { roomId, text: input });
    setInput('');
    socket.emit('typing:stop', roomId);
  }, [input, roomId]);

  const handleTyping = useCallback((e: React.ChangeEvent<HTMLInputElement>) => {
    setInput(e.target.value);
    socket.emit('typing:start', roomId);

    // Debounce typing stop
    const timeout = setTimeout(() => {
      socket.emit('typing:stop', roomId);
    }, 1000);

    return () => clearTimeout(timeout);
  }, [roomId]);

  return (
    <div className="chat-room">
      <div className="messages">
        {messages.map((msg) => (
          <div key={msg.id} className="message">
            <strong>{msg.userName}:</strong> {msg.text}
          </div>
        ))}
      </div>

      {typing.length > 0 && (
        <div className="typing">
          {typing.join(', ')} {typing.length === 1 ? 'is' : 'are'} typing...
        </div>
      )}

      <div className="input-area">
        <input
          value={input}
          onChange={handleTyping}
          onKeyPress={(e) => e.key === 'Enter' && sendMessage()}
          placeholder="Type a message..."
        />
        <button onClick={sendMessage}>Send</button>
      </div>
    </div>
  );
}

Scaling WebSockets

Redis Adapter

import { createAdapter } from '@socket.io/redis-adapter';
import { createClient } from 'redis';

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

await Promise.all([pubClient.connect(), subClient.connect()]);

io.adapter(createAdapter(pubClient, subClient));

// Now works across multiple servers
io.emit('event', data); // Broadcasts to all connected clients across all servers

Sticky Sessions (Load Balancer)

# nginx.conf
upstream socket_servers {
    ip_hash;  # Sticky sessions
    server 127.0.0.1:3001;
    server 127.0.0.1:3002;
    server 127.0.0.1:3003;
}

server {
    location /socket.io/ {
        proxy_pass http://socket_servers;
        proxy_http_version 1.1;
        proxy_set_header Upgrade $http_upgrade;
        proxy_set_header Connection "upgrade";
        proxy_set_header Host $host;
    }
}

Error Handling

// Server
io.on('connection', (socket) => {
  socket.on('error', (error) => {
    console.error('Socket error:', error);
  });

  // Wrap handlers in try-catch
  socket.on('message', async (data, callback) => {
    try {
      const result = await processMessage(data);
      callback({ success: true, data: result });
    } catch (error) {
      callback({ success: false, error: error.message });
    }
  });
});

// Client
socket.on('connect_error', (error) => {
  console.error('Connection error:', error);
});

// With acknowledgment
socket.emit('message', data, (response) => {
  if (response.success) {
    console.log('Message sent:', response.data);
  } else {
    console.error('Failed:', response.error);
  }
});

Summary

FeatureUse Case
EventsSend/receive messages
RoomsGroup communication
NamespacesSeparate concerns
AcknowledgmentsConfirm delivery
Redis AdapterScale horizontally
AuthenticationSecure connections

WebSockets enable real-time, bidirectional communication essential for modern interactive applications.

Advertisement

MR

Moshiour Rahman

Software Architect & AI Engineer

Share:
MR

Moshiour Rahman

Software Architect & AI Engineer

Enterprise software architect with deep expertise in financial systems, distributed architecture, and AI-powered applications. Building large-scale systems at Fortune 500 companies. Specializing in LLM orchestration, multi-agent systems, and cloud-native solutions. I share battle-tested patterns from real enterprise projects.

Related Articles

Comments

Comments are powered by GitHub Discussions.

Configure Giscus at giscus.app to enable comments.