Implement Real-Time Messaging
Create the message event, send messages from the browser, broadcast them to connected users, and understand different broadcasting strategies.
Create the Chat Event Handler
We will now separate chat event handling from the main server configuration.
Create src/events/chat.events.js.
This module receives the Socket.IO server instance and registers event listeners for each client.
The first event we will implement is chat:send.
Whenever a client emits this event, the server receives the message payload and broadcasts it to connected clients.
export function registerChatEvents(io) {
io.on('connection', (socket) => {
console.log('Client connected:', socket.id);
socket.on('chat:send', (payload) => {
console.log('Received message:', payload);
});
socket.on('disconnect', (reason) => {
console.log('Client disconnected:', socket.id, reason);
});
});
}Creates a reusable event registration function. Each connected socket receives its own chat:send and disconnect event listeners.
Keep event handlers separate from server initialization.
Register client-specific listeners inside the connection callback.
Use descriptive event names.
Register Chat Events
Import the chat event registration function into src/server.js.
After initializing Socket.IO, register the event handlers using the io instance.
import 'dotenv/config';
import express from 'express';
import { createServer } from 'node:http';
import { initializeSocket } from './config/socket.js';
import { registerChatEvents } from './events/chat.events.js';
const app = express();
app.use(express.json());
app.use(express.static('public'));
app.get('/health', (req, res) => {
res.json({ status: 'ok' });
});
const httpServer = createServer(app);
const io = initializeSocket(httpServer);
registerChatEvents(io);
httpServer.listen(process.env.PORT || 3000, () => {
console.log('Chat application running');
});Initializes the server and registers all chat-related Socket.IO events before accepting connections.
The event module is registered using the Socket.IO instance.
The server file remains responsible for application startup.
Event registration should happen before accepting client connections.
Send a Message from the Browser
Open public/app.js and implement the message submission logic.
The browser needs to:
- Read the username.
- Read the message input.
- Validate that the values are not empty.
- Emit the
chat:sendevent. - Clear the message input after sending.
The browser should send a structured object rather than a plain string.
Example payload:
{
"username": "John",
"message": "Hello everyone!"
}
Browser-side validation improves user experience, but never trust client input. The server must independently validate usernames, message content, and payload size.
const usernameInput = document.getElementById('username');
const joinButton = document.getElementById('join-button');
const messageForm = document.getElementById('message-form');
const messageInput = document.getElementById('message-input');
joinButton.addEventListener('click', () => {
const username = usernameInput.value.trim();
if (!username) {
alert('Enter your username');
return;
}
usernameInput.disabled = true;
joinButton.disabled = true;
messageInput.disabled = false;
messageForm.querySelector('button').disabled = false;
});
messageForm.addEventListener('submit', (event) => {
event.preventDefault();
const username = usernameInput.value.trim();
const message = messageInput.value.trim();
if (!username || !message) return;
socket.emit('chat:send', {
username,
message
});
messageInput.value = '';
});The form submission handler validates the message and emits a structured payload to the server.
socket.emit() sends data to the server.
Use structured payloads for application events.
Validate user input on both client and server.
Validate Incoming Messages on the Server
The server must validate every incoming message because users can bypass browser-side validation.
For this application, we will enforce:
- Username must be a non-empty string.
- Message must be a non-empty string.
- Username must not exceed 30 characters.
- Message must not exceed 1000 characters.
- Unexpected payload types must be rejected.
Update the chat event handler.
function validateChatMessage(payload) {
if (!payload || typeof payload !== 'object') {
return false;
}
if (typeof payload.username !== 'string') {
return false;
}
if (typeof payload.message !== 'string') {
return false;
}
const username = payload.username.trim();
const message = payload.message.trim();
return (
username.length > 0 &&
username.length <= 30 &&
message.length > 0 &&
message.length <= 1000
);
}
export function registerChatEvents(io) {
io.on('connection', (socket) => {
socket.on('chat:send', (payload) => {
if (!validateChatMessage(payload)) {
socket.emit('chat:error', {
message: 'Invalid message payload'
});
return;
}
console.log('Valid message:', payload);
});
});
}The validation function verifies the payload structure, data types, and maximum lengths before processing the message.
Never trust incoming Socket.IO payloads.
Validate object structure and data types.
Apply reasonable limits to incoming messages.
Broadcast Messages to All Connected Users
Now that the server can receive messages, we need to distribute them to other connected users.
Socket.IO provides multiple broadcasting methods.
The io.emit() method sends an event to all connected clients, including the sender.
The socket.broadcast.emit() method sends an event to all connected clients except the sender.
For our chat application, we will use io.emit() so the sender also receives the published message through the same event handler.
Broadcasting Methods
All connected clients
Includes the sender.
Current socket
Sends an event only to the socket represented by this object.
All except sender
Excludes the current socket.
export function registerChatEvents(io) {
io.on('connection', (socket) => {
socket.on('chat:send', (payload) => {
if (!validateChatMessage(payload)) {
socket.emit('chat:error', {
message: 'Invalid message payload'
});
return;
}
const chatMessage = {
username: payload.username.trim(),
message: payload.message.trim(),
timestamp: new Date().toISOString()
};
io.emit('chat:message', chatMessage);
});
});
}
function validateChatMessage(payload) {
return Boolean(
payload &&
typeof payload.username === 'string' &&
typeof payload.message === 'string' &&
payload.username.trim().length > 0 &&
payload.username.trim().length <= 30 &&
payload.message.trim().length > 0 &&
payload.message.trim().length <= 1000
);
}The server validates incoming data, creates a timestamped message object, and broadcasts it to all connected clients.
io.emit() broadcasts to every connected client in the namespace.
socket.broadcast.emit() excludes the current sender.
Server-generated timestamps provide consistent message metadata.
Receive and Display Messages in the Browser
The server now broadcasts messages using the chat:message event.
The browser needs to listen for that event and update the message container.
Create a reusable function that generates a message element.
Use textContent instead of innerHTML to display user-generated content safely. This prevents message content from being interpreted as HTML.
Never insert untrusted message content directly through innerHTML. Use textContent or a trusted HTML sanitization strategy.
const messagesContainer = document.getElementById('messages');
socket.on('chat:message', (chatMessage) => {
const messageElement = document.createElement('div');
messageElement.className = 'message';
const usernameElement = document.createElement('strong');
usernameElement.textContent = `${chatMessage.username}: `;
const contentElement = document.createElement('span');
contentElement.textContent = chatMessage.message;
const timeElement = document.createElement('small');
timeElement.textContent = ` ${new Date(chatMessage.timestamp).toLocaleTimeString()}`;
messageElement.append(
usernameElement,
contentElement,
timeElement
);
messagesContainer.appendChild(messageElement);
messagesContainer.scrollTop = messagesContainer.scrollHeight;
});
socket.on('chat:error', (error) => {
console.error(error.message);
alert(error.message);
});Listens for incoming chat messages, creates DOM elements safely, displays the username and message, and scrolls the chat container to the latest message.
socket.on() receives server events.
DOM elements can be created dynamically to display messages.
Use textContent for untrusted user-generated content.
Test Real-Time Messaging
The first complete real-time messaging workflow is now implemented.
Restart the backend if necessary and open the application in two browser windows.
Follow these steps:
- Open the first browser window.
- Enter username John.
- Join the chat.
- Open the second browser window.
- Enter username Sarah.
- Join the chat.
- Send a message from John.
- Verify that Sarah receives it immediately.
- Send another message from Sarah.
- Verify that John receives it.
Expected result:
Both clients should see the same messages without refreshing the browser.
At this stage, usernames are supplied by the browser and are not authenticated identities. We will discuss trusted identity and authentication in a later chapter.
Multiple browser clients can exchange messages in real time.
Messages are distributed through Socket.IO broadcasting.
The application does not yet persist messages after server restarts.