7 Topics
Chapter•45 Minutes

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.

src/events/chat.events.js
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.

Key Takeaways
  • 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.

Updated src/server.js
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.

Key Takeaways
  • 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:

  1. Read the username.
  2. Read the message input.
  3. Validate that the values are not empty.
  4. Emit the chat:send event.
  5. 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!"
}
Client Validation Is Not Enough

Browser-side validation improves user experience, but never trust client input. The server must independently validate usernames, message content, and payload size.

Message Submission
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.

Key Takeaways
  • 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.

Validate Chat Payload
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.

Key Takeaways
  • 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

io.emit()

All connected clients

Includes the sender.

socket.emit()

Current socket

Sends an event only to the socket represented by this object.

socket.broadcast.emit()

All except sender

Excludes the current socket.

Broadcast Chat Messages
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.

Key Takeaways
  • 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.

Prevent Cross-Site Scripting

Never insert untrusted message content directly through innerHTML. Use textContent or a trusted HTML sanitization strategy.

Receive Chat Messages
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.

Key Takeaways
  • 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:

  1. Open the first browser window.
  2. Enter username John.
  3. Join the chat.
  4. Open the second browser window.
  5. Enter username Sarah.
  6. Join the chat.
  7. Send a message from John.
  8. Verify that Sarah receives it immediately.
  9. Send another message from Sarah.
  10. 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.

Key Takeaways
  • 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.