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.
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
| HTTP | WebSocket |
|---|---|
| Request-Response | Bidirectional |
| Stateless | Stateful |
| New connection each request | Persistent connection |
| High latency | Low latency |
| Polling for updates | Push 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
| Feature | Use Case |
|---|---|
| Events | Send/receive messages |
| Rooms | Group communication |
| Namespaces | Separate concerns |
| Acknowledgments | Confirm delivery |
| Redis Adapter | Scale horizontally |
| Authentication | Secure connections |
WebSockets enable real-time, bidirectional communication essential for modern interactive applications.
Advertisement
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
GraphQL API Development: Complete Guide with Node.js
Master GraphQL API development from scratch. Learn schema design, resolvers, queries, mutations, subscriptions, and authentication best practices.
JavaScriptMongoDB with Node.js: Complete Database Guide
Master MongoDB with Node.js and Mongoose. Learn CRUD operations, schema design, indexing, aggregation pipelines, and production best practices.
JavaScriptBun: The Fast JavaScript Runtime and Toolkit
Master Bun for JavaScript development. Learn the runtime, package manager, bundler, test runner, and build faster applications with Bun.
Comments
Comments are powered by GitHub Discussions.
Configure Giscus at giscus.app to enable comments.