Real-time chat with Socket.IO, Redis and Mongo DB

This project is a proof of concept of a real-time chat system with support for multiple rooms, message history and notifications. It is developed with technologies designed to scale in a microservices-based architecture.

Technologies Applied

  • Node.js + Express: HTTP server and route management.
  • Socket.IO: Real-time communication with WebSockets.
  • MongoDB: Persistence of messages and rooms.
  • Redis: Pub/sub system to distribute messages between multiple server instances.
  • HTML + JS: Cliente web simple.
  • PrimeFlex (opcional): Visual enhancement of the interface.

Why Redis

Redis is used to enable the posting and subscription of messages between multiple processes or server instances. This is necessary when the server is running in a distributed or balanced environment, as Socket.IO alone does not share information between instances.

Using Redis ensures that all connected users receive messages even if they are served by different processes or servers.

Project Structure: chat_0.1

This project has an organized architecture for developing a chat application using Node.js with TypeScript and Redis. The main directories and files are described below:

Directorios

  • .git/
    Git repository configuration folder. Contains version history and version control settings.
  • node_modules/
    Directory automatically generated by npm, contains all project dependencies.
  • src/
    Main folder of the application source code.
    • public/
      Directory optionally used for public files (such as HTML, CSS or JS accessible directly).
    • index.html
      This file is the main page displayed when opening the application in the browser. It serves as a visual entry point and typically contains the basic design of the interface, in addition to the scripts needed for the application to work.
<!DOCTYPE html>
<html lang="es">
<head>
  <meta charset="UTF-8" />
  <title>Chat en Tiempo Real</title>
  <link rel="stylesheet" href="https://unpkg.com/primeflex@3.3.1/primeflex.min.css">
  <link rel="stylesheet" href="https://unpkg.com/primeicons/primeicons.css">
  <style>
    body {
      font-family: sans-serif;
      padding: 2rem;
    }
    #chatBox {
      border: 1px solid #ccc;
      height: 250px;
      overflow-y: auto;
      padding: 1rem;
      background: #f4f4f4;
      border-radius: 6px;
    }
    #chatBox p {
      margin: 0.5rem 0;
    }
  </style>
</head>
<body>
  <h2 class="mb-3">💬 Chat en Tiempo Real</h2>

  <div class="flex flex-column gap-2 mb-3">
    <input id="username" class="p-inputtext p-component" placeholder="Tu nombre" />
    <input id="roomName" class="p-inputtext p-component" placeholder="Nombre de la sala" />
    <button class="p-button p-component" onclick="joinRoom()">Entrar</button>
  </div>

  <h3>Salas disponibles:</h3>
  <ul id="roomList" class="mb-3"></ul>

  <div id="chat" style="display:none;">
    <h3>Chat en Sala</h3>
    <div id="chatBox" class="mb-2"></div>
    <div class="flex gap-2">
      <input id="inputMsg" class="p-inputtext p-component flex-1" placeholder="Mensaje..." />
      <button class="p-button p-component" onclick="sendMessage()">Enviar</button>
    </div>
  </div>

  <audio id="notifSound" src="https://notificationsounds.com/storage/sounds/file-sounds-1154-pristine.mp3" preload="auto"></audio>

  <script src="https://cdn.socket.io/4.7.2/socket.io.min.js"></script>
  <script>
    const socket = io();
    let currentRoom = '';

    // Pedir permiso para notificaciones
    if (Notification.permission !== 'granted') {
      Notification.requestPermission();
    }

    async function joinRoom() {
      const room = document.getElementById('roomName').value;
      const username = document.getElementById('username').value;
      if (!room || !username) return alert('Ingresa nombre y sala');

      currentRoom = room;
      document.getElementById('chat').style.display = 'block';
      clearMessages();

      socket.emit('joinRoom', room, username);

      socket.off('chatMessage');
      socket.on('chatMessage', (data) => {
        addMessage(`${data.user}: ${data.message}`);
        playNotificationSound();
        showNotification(data.user, data.message);
      });

      socket.off('notification');
      socket.on('notification', (msg) => {
        addMessage(`🔔 ${msg}`);
      });

      socket.off('chat-history');
      socket.on('chat-history', (messages) => {
        messages.forEach(msg => {
          addMessage(`${msg.user}: ${msg.message}`);
        });
      });

      addMessage(`🟢 Te uniste a la sala: ${room}`);
    }

    function sendMessage() {
      const message = document.getElementById('inputMsg').value;
      const user = document.getElementById('username').value;
      if (!message.trim()) return;
      socket.emit('chatMessage', { room: currentRoom, user, message });
      document.getElementById('inputMsg').value = '';
    }

    function addMessage(msg) {
      const box = document.getElementById('chatBox');
      const p = document.createElement('p');
      p.textContent = msg;
      box.appendChild(p);
      box.scrollTop = box.scrollHeight;
    }

    function clearMessages() {
      document.getElementById('chatBox').innerHTML = '';
    }

    async function fetchRooms() {
      try {
        const res = await fetch('/rooms');
        const rooms = await res.json();
        const list = document.getElementById('roomList');
        list.innerHTML = '';
        rooms.forEach(r => {
          const li = document.createElement('li');
          li.textContent = r.name;
          list.appendChild(li);
        });
      } catch (err) {
        console.error('Error al cargar salas:', err);
      }
    }

    function showNotification(user, message) {
      if (Notification.permission === 'granted') {
        new Notification(`💬 ${user}`, {
          body: message,
          icon: 'https://cdn-icons-png.flaticon.com/512/2331/2331942.png'
        });
      }
    }

    function playNotificationSound() {
      const sound = document.getElementById('notifSound');
      sound.currentTime = 0;
      sound.play().catch(() => {});
    }

    fetchRooms();
  </script>
</body>
</html>
  • db.ts
    Configuration file and connection to the database (possibly MongoDB, PostgreSQL or other).
import mongoose from 'mongoose';

await mongoose.connect('mongodb://localhost:27017/chat-app');

const messageSchema = new mongoose.Schema({
  room: String,
  user: String,
  message: String,
  timestamp: { type: Date, default: Date.now }
});

const roomSchema = new mongoose.Schema({
  name: { type: String, unique: true }
});

export const Message = mongoose.model('Message', messageSchema);
export const Room = mongoose.model('Room', roomSchema);
  • index.ts
    Main entry point of the application. It is where the server or system core is initialized and booted.
import express from 'express';
import http from 'http';
import { Server } from 'socket.io';
import { pub, sub } from './redis.js';
import { Message, Room } from './db.js';
import path from 'path';
import { fileURLToPath } from 'url';

const app = express();
const server = http.createServer(app);
const io = new Server(server);
const __dirname = path.dirname(fileURLToPath(import.meta.url));

app.use(express.static(path.join(__dirname, 'public')));
app.use(express.json());

// Listar salas existentes
app.get('/rooms', async (_req, res) => {
  const rooms = await Room.find().select('name -_id');
  res.json(rooms);
});

io.on('connection', (socket) => {
  console.log('Cliente conectado');

  socket.on('joinRoom', async (room, username) => {
    socket.join(room);
    // Enviar historial al nuevo usuario
    const history = await Message.find({ room }).sort({ timestamp: 1 }).lean();
    socket.emit('chat-history', history);

    // Crear sala si no existe
    const exists = await Room.findOne({ name: room });
    if (!exists) {
      await new Room({ name: room }).save();
    }

    socket.to(room).emit('notification', `${username} se unió al chat`);
  });

  socket.on('chatMessage', async (data) => {
    const { room, user, message } = data;
    const chatMessage = { room, user, message };

    await pub.publish('chat', JSON.stringify(chatMessage));
    await new Message(chatMessage).save();
  });
});

await sub.subscribe('chat', (msg) => {
  const parsed = JSON.parse(msg);
  io.to(parsed.room).emit('chatMessage', parsed);
});

server.listen(3000,'0.0.0.0', () => {
  console.log('Servidor en http://localhost:3000');
});
  • redis.ts
    Configuration and connection logic with Redis, probably used for caching or real-time session/chat management.
import { createClient } from 'redis';

export const pub = createClient();
export const sub = createClient();

await pub.connect();
await sub.connect();
  • types.ts
    File that defines custom TypeScript types and structures to keep typing strong and organized.
export interface ChatMessage {
  room: string;
  user: string;
  message: string;
}

export interface ChatRoom {
  name: string;
}

Configuration files

  • .gitignore
    List of files and folders that Git should ignore (for example: node_modules, logs, claves, etc.).
  • docker-compose.yml
    Configuration file for Docker Compose. Defines the services and containers required to run the application in a container environment (such as Node, Redis, etc.).
  • package.json
    Main configuration file of the Node.js project. Contains project information, scripts, and dependencies.
  • package-lock.json
    Automatically generated file to lock down exact versions of dependencies, ensuring consistent environments.
  • README.md
    Project documentation file. It usually contains instructions for installation, use and purpose of the system.
  • tsconfig.json
    Configuration of the TypeScript compiler. Defines how the project should be compiled (paths, target, module, etc.).

Funcionalidades

  • Create or join rooms from the frontend
  • Listar salas disponibles
  • Sending and receiving messages in real time
  • Receive message history when entering a room
  • MongoDB Message Persistence
  • Notifications within the chat
  • Local network exposure using 0.0.0.0

Use source code:

GITHUB

¡Mensaje enviado!