
Real-time communication is a core requirement for modern applications—from chat apps to live notifications to collaborative tools. With Node.js and WebSocket, you can build a lightweight chat server that delivers messages instantly without relying on third-party services.
In this guide, you’ll learn how to build a complete Node.js WebSocket chat server from scratch. We’ll cover the WebSocket protocol, implement a broadcast server, add user management, handle edge cases, and explore production considerations.
Why Use WebSocket for Real-Time Chat?
Traditional HTTP is request-response based. The client sends a request, the server responds, and the connection closes. For real-time features, this model falls short—you’d need constant polling, which wastes bandwidth and introduces latency.
WebSocket establishes a persistent, bidirectional connection. Once opened, both client and server can send messages at any time without the overhead of new HTTP requests.
HTTP vs WebSocket Comparison
| Aspect | HTTP Polling | WebSocket |
|---|---|---|
| Connection | New for each request | Persistent |
| Latency | Polling interval delay | Near-instant |
| Bandwidth | High (repeated headers) | Low (small frames) |
| Server Push | Not supported | Native support |
| Complexity | Simple to implement | Slightly more setup |
WebSocket is ideal for:
- Chat applications
- Live notifications
- Real-time dashboards
- Multiplayer games
- Collaborative editing
Step 1: Project Setup
Create a new directory and initialize a Node.js project:
mkdir websocket-chat
cd websocket-chat
npm init -y
Install the required dependencies:
npm install ws uuid
The ws package is a simple, fast WebSocket implementation for Node.js. The uuid package helps generate unique identifiers for users.
Create the project structure:
websocket-chat/
├── server.js # Main server file
├── client.html # Test client
├── package.json
└── package-lock.json
Step 2: Create the Basic Chat Server
Start with a basic server that broadcasts messages to all connected clients:
// server.js
const WebSocket = require('ws');
const { v4: uuidv4 } = require('uuid');
const PORT = process.env.PORT || 8080;
const wss = new WebSocket.Server({ port: PORT });
// Store connected clients with metadata
const clients = new Map();
console.log(`WebSocket server started on ws://localhost:${PORT}`);
wss.on('connection', (ws, request) => {
// Generate unique ID for this client
const clientId = uuidv4();
const clientIp = request.socket.remoteAddress;
// Store client with metadata
clients.set(ws, {
id: clientId,
ip: clientIp,
username: `User-${clientId.slice(0, 4)}`,
connectedAt: new Date()
});
console.log(`Client connected: ${clientId} from ${clientIp}`);
console.log(`Total clients: ${clients.size}`);
// Send welcome message to new client
ws.send(JSON.stringify({
type: 'system',
message: `Welcome! You are connected as ${clients.get(ws).username}`,
clientId: clientId,
timestamp: new Date().toISOString()
}));
// Notify others about new user
broadcast(ws, {
type: 'system',
message: `${clients.get(ws).username} joined the chat`,
timestamp: new Date().toISOString()
});
// Handle incoming messages
ws.on('message', (data) => {
handleMessage(ws, data);
});
// Handle client disconnect
ws.on('close', () => {
const client = clients.get(ws);
console.log(`Client disconnected: ${client.id}`);
// Notify others about user leaving
broadcast(ws, {
type: 'system',
message: `${client.username} left the chat`,
timestamp: new Date().toISOString()
});
clients.delete(ws);
console.log(`Total clients: ${clients.size}`);
});
// Handle errors
ws.on('error', (error) => {
console.error(`WebSocket error for ${clientId}:`, error.message);
});
});
function handleMessage(senderWs, data) {
const client = clients.get(senderWs);
try {
const message = JSON.parse(data.toString());
switch (message.type) {
case 'chat':
// Broadcast chat message to all other clients
broadcast(senderWs, {
type: 'chat',
username: client.username,
message: message.content,
timestamp: new Date().toISOString()
});
break;
case 'setUsername':
// Update username
const oldUsername = client.username;
client.username = message.username.slice(0, 20); // Limit length
broadcast(null, {
type: 'system',
message: `${oldUsername} is now known as ${client.username}`,
timestamp: new Date().toISOString()
});
break;
default:
console.log(`Unknown message type: ${message.type}`);
}
} catch (error) {
console.error('Error parsing message:', error.message);
// Handle plain text messages
broadcast(senderWs, {
type: 'chat',
username: client.username,
message: data.toString(),
timestamp: new Date().toISOString()
});
}
}
function broadcast(excludeWs, message) {
const messageStr = JSON.stringify(message);
clients.forEach((clientData, clientWs) => {
if (clientWs !== excludeWs && clientWs.readyState === WebSocket.OPEN) {
clientWs.send(messageStr);
}
});
}
// Graceful shutdown
process.on('SIGTERM', () => {
console.log('Shutting down server...');
wss.clients.forEach((ws) => {
ws.close(1001, 'Server shutting down');
});
wss.close(() => {
console.log('Server closed');
process.exit(0);
});
});
This server includes several production-ready features:
- Unique client identification
- Username management
- Structured JSON messages
- System notifications for join/leave events
- Error handling
- Graceful shutdown
Step 3: Create the Web Client
Build a complete HTML client with a better user interface:
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>WebSocket Chat</title>
<style>
* { box-sizing: border-box; margin: 0; padding: 0; }
body {
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
background: #1a1a2e;
color: #eee;
height: 100vh;
display: flex;
flex-direction: column;
}
.header {
background: #16213e;
padding: 1rem;
display: flex;
justify-content: space-between;
align-items: center;
}
.status {
padding: 0.5rem 1rem;
border-radius: 20px;
font-size: 0.875rem;
}
.status.connected { background: #00b894; }
.status.disconnected { background: #d63031; }
.messages {
flex: 1;
overflow-y: auto;
padding: 1rem;
}
.message {
margin-bottom: 0.75rem;
padding: 0.75rem;
border-radius: 8px;
max-width: 80%;
}
.message.chat { background: #16213e; }
.message.system {
background: transparent;
color: #888;
font-style: italic;
text-align: center;
max-width: 100%;
}
.message .username {
font-weight: bold;
color: #74b9ff;
margin-bottom: 0.25rem;
}
.message .time {
font-size: 0.75rem;
color: #666;
margin-top: 0.25rem;
}
.input-area {
background: #16213e;
padding: 1rem;
display: flex;
gap: 0.5rem;
}
input {
flex: 1;
padding: 0.75rem;
border: none;
border-radius: 8px;
background: #0f0f23;
color: #eee;
font-size: 1rem;
}
button {
padding: 0.75rem 1.5rem;
border: none;
border-radius: 8px;
background: #0984e3;
color: white;
cursor: pointer;
font-size: 1rem;
}
button:hover { background: #74b9ff; }
button:disabled { background: #555; cursor: not-allowed; }
</style>
</head>
<body>
<div class="header">
<h1>WebSocket Chat</h1>
<span id="status" class="status disconnected">Disconnected</span>
</div>
<div id="messages" class="messages"></div>
<div class="input-area">
<input id="messageInput" placeholder="Type a message..." autocomplete="off">
<button id="sendBtn" onclick="sendMessage()">Send</button>
</div>
<script>
let socket;
let reconnectAttempts = 0;
const maxReconnectAttempts = 5;
function connect() {
const wsUrl = 'ws://localhost:8080';
socket = new WebSocket(wsUrl);
socket.onopen = () => {
console.log('Connected to server');
reconnectAttempts = 0;
updateStatus(true);
};
socket.onmessage = (event) => {
const data = JSON.parse(event.data);
displayMessage(data);
};
socket.onclose = (event) => {
console.log('Disconnected:', event.reason);
updateStatus(false);
attemptReconnect();
};
socket.onerror = (error) => {
console.error('WebSocket error:', error);
};
}
function attemptReconnect() {
if (reconnectAttempts < maxReconnectAttempts) {
reconnectAttempts++;
const delay = Math.min(1000 * Math.pow(2, reconnectAttempts), 30000);
console.log(`Reconnecting in ${delay/1000}s (attempt ${reconnectAttempts})`);
setTimeout(connect, delay);
}
}
function updateStatus(connected) {
const status = document.getElementById('status');
status.textContent = connected ? 'Connected' : 'Disconnected';
status.className = 'status ' + (connected ? 'connected' : 'disconnected');
document.getElementById('sendBtn').disabled = !connected;
}
function displayMessage(data) {
const messages = document.getElementById('messages');
const div = document.createElement('div');
div.className = `message ${data.type}`;
if (data.type === 'chat') {
div.innerHTML = `
<div class="username">${escapeHtml(data.username)}</div>
<div>${escapeHtml(data.message)}</div>
<div class="time">${formatTime(data.timestamp)}</div>
`;
} else {
div.textContent = data.message;
}
messages.appendChild(div);
messages.scrollTop = messages.scrollHeight;
}
function sendMessage() {
const input = document.getElementById('messageInput');
const message = input.value.trim();
if (message && socket.readyState === WebSocket.OPEN) {
// Check for commands
if (message.startsWith('/name ')) {
socket.send(JSON.stringify({
type: 'setUsername',
username: message.slice(6)
}));
} else {
socket.send(JSON.stringify({
type: 'chat',
content: message
}));
// Display own message locally
displayMessage({
type: 'chat',
username: 'You',
message: message,
timestamp: new Date().toISOString()
});
}
input.value = '';
}
}
function escapeHtml(text) {
const div = document.createElement('div');
div.textContent = text;
return div.innerHTML;
}
function formatTime(timestamp) {
return new Date(timestamp).toLocaleTimeString();
}
// Handle Enter key
document.getElementById('messageInput').addEventListener('keypress', (e) => {
if (e.key === 'Enter') sendMessage();
});
// Connect on page load
connect();
</script>
</body>
</html>
This client includes:
- Automatic reconnection with exponential backoff
- Connection status indicator
- Username change via
/namecommand - XSS protection through HTML escaping
- Keyboard support (Enter to send)
Step 4: Running the Chat Server
Start the server:
node server.js
Open client.html in multiple browser tabs to simulate different users. Type messages and watch them appear in real-time across all connected clients.
Step 5: Adding Chat Rooms
For more complex applications, implement chat rooms:
// Room management
const rooms = new Map();
function joinRoom(ws, roomName) {
const client = clients.get(ws);
// Leave current room
if (client.room) {
leaveRoom(ws, client.room);
}
// Join new room
if (!rooms.has(roomName)) {
rooms.set(roomName, new Set());
}
rooms.get(roomName).add(ws);
client.room = roomName;
// Notify room
broadcastToRoom(roomName, null, {
type: 'system',
message: `${client.username} joined ${roomName}`,
timestamp: new Date().toISOString()
});
}
function leaveRoom(ws, roomName) {
const client = clients.get(ws);
const room = rooms.get(roomName);
if (room) {
room.delete(ws);
broadcastToRoom(roomName, ws, {
type: 'system',
message: `${client.username} left ${roomName}`,
timestamp: new Date().toISOString()
});
// Clean up empty rooms
if (room.size === 0) {
rooms.delete(roomName);
}
}
}
function broadcastToRoom(roomName, excludeWs, message) {
const room = rooms.get(roomName);
if (!room) return;
const messageStr = JSON.stringify(message);
room.forEach((ws) => {
if (ws !== excludeWs && ws.readyState === WebSocket.OPEN) {
ws.send(messageStr);
}
});
}
Production Considerations
Before deploying to production, consider these important factors:
Scaling with Multiple Servers
A single WebSocket server can’t scale horizontally without additional infrastructure. Use Redis Pub/Sub or a message broker to synchronize messages across multiple server instances.
Authentication
Add authentication during the WebSocket handshake:
wss.on('connection', (ws, request) => {
// Verify token from query string or cookie
const token = new URL(request.url, 'http://localhost').searchParams.get('token');
if (!verifyToken(token)) {
ws.close(4001, 'Unauthorized');
return;
}
// Continue with connection setup...
});
Rate Limiting
Prevent abuse by limiting message frequency per client. Track message counts and temporarily block clients who exceed limits.
Message Persistence
For message history, store messages in a database (MongoDB, PostgreSQL) and load recent messages when clients connect.
Conclusion
You’ve built a complete Node.js WebSocket chat server with user management, structured messaging, and a functional web client. This foundation supports more advanced features like chat rooms, file sharing, message persistence, and authentication.
WebSocket’s persistent connection model makes it ideal for real-time applications. Combined with Node.js’s event-driven architecture, you can build scalable chat systems that handle thousands of concurrent connections.
For more backend development guides, see our posts on Building REST APIs with FastAPI and Mastering Async/Await in JavaScript. For WebSocket documentation, refer to the MDN WebSocket guide.
2 Comments