100% Free Forever
AI-Powered Learning
Industry Expert Content
Certificates & Badges
Learn At Your Own Pace
Programming

Building a Chat Application

Learn how to design a real-time chat app with Socket.IO, covering rooms, broadcasting, message persistence, and delivery guarantees.

Practical Socket.IOIntermediate9 min readJul 10, 2026
Analogies

Building a Chat Application with Socket.IO

A Socket.IO chat application pairs a persistent WebSocket connection with a small set of custom events: the server creates an io instance attached to an HTTP server, and each connected client gets a socket object used to send and receive messages. Instead of polling an endpoint, the client emits a chat:message event and every other connected client receives it instantly through a listener registered with socket.on.

🏏

Cricket analogy: Setting up the io instance is like a stadium's PA system going live before a match — once wired in, every fielder (client) hears the umpire's call (server broadcast) the instant it happens, no need to check a scoreboard app.

Rooms and Message Broadcasting

Real chat apps rarely broadcast to everyone; they need channels. Socket.IO models this with rooms — arbitrary string labels a socket joins via socket.join('room-42'). To send a message to only that room, the server calls io.to('room-42').emit('chat:message', payload), and socket.to(room) does the same while excluding the sender, which is the correct pattern for echoing a sender's own message back with socket.emit separately so the UI doesn't show it twice.

🏏

Cricket analogy: It's like a broadcaster's regional feed — fans tuned to the Chennai commentary room hear the Tamil commentary, while the Mumbai room gets Marathi, even though it's the same live match underneath.

Persisting Chat History

Socket.IO only handles live delivery; it does not store anything, so a production chat app persists each message to a database (commonly via an async write inside the chat:message handler) and replays recent history when a user joins a room, typically by querying the last N messages and emitting them as a single chat:history event right after the join acknowledgment. This separation keeps the real-time layer thin while the durability guarantee lives in the data layer, which also lets you support features like message search that Socket.IO itself cannot provide.

🏏

Cricket analogy: It's like a scorer's book alongside the live commentary — the radio broadcast tells you what's happening now, but the printed scorecard is what a fan checks the next morning to see every over that was bowled.

Typing Indicators and Delivery Feedback

Ephemeral UI signals like typing indicators should not clog the persistence layer or guarantee delivery, so Socket.IO's volatile emit is the right tool: socket.volatile.emit('chat:typing', { userId }) drops the packet if the client's buffer is backed up rather than queuing it, which is exactly the behavior wanted since a stale 'is typing' event a few seconds late is worse than useless. For actual message delivery you instead want acknowledgments — the client calls socket.emit('chat:message', payload, (ack) => {...}) and the server invokes the callback once the message is safely handled, letting the UI show a checkmark only after real confirmation.

🏏

Cricket analogy: It's like a fielder's quick hand signal to the keeper mid-over versus the official third-umpire review — the hand signal can be missed with no consequence, but a run-out review must get a definitive, confirmed answer before play resumes.

javascript
// server.js
const { Server } = require('socket.io');
const io = new Server(httpServer, { cors: { origin: '*' } });

io.on('connection', (socket) => {
  socket.on('join', async ({ roomId, userId }, ack) => {
    socket.join(roomId);
    socket.data.userId = userId;
    const history = await ChatMessage.find({ roomId }).sort({ createdAt: -1 }).limit(50);
    socket.emit('chat:history', history.reverse());
    ack({ ok: true });
  });

  socket.on('chat:message', async ({ roomId, text }, ack) => {
    const message = await ChatMessage.create({ roomId, text, userId: socket.data.userId });
    io.to(roomId).emit('chat:message', message);
    ack({ ok: true, id: message._id });
  });

  socket.on('chat:typing', ({ roomId, userId }) => {
    socket.volatile.to(roomId).emit('chat:typing', { userId });
  });
});

Never trust a userId or username sent directly in the message payload from the client — attach the identity to socket.data during authentication (e.g. in a connection middleware validating a JWT) and read it from there when persisting messages, otherwise any client can impersonate another user.

  • Socket.IO delivers events instantly over a persistent connection but stores nothing itself — persistence is your database's job.
  • Rooms (socket.join, io.to(room).emit) scope broadcasts to a subset of connected clients such as a chat channel.
  • socket.to(room) excludes the sender, useful when the sender's UI updates optimistically and shouldn't receive an echo.
  • Load recent chat history from the database and emit it right after a successful join, since Socket.IO has no memory of past events.
  • Use socket.volatile.emit for ephemeral signals like typing indicators where a dropped packet is acceptable.
  • Use acknowledgment callbacks (socket.emit(event, payload, ack)) when the client needs a guaranteed confirmation, such as message delivery.
  • Always derive user identity from server-side session/auth data attached to socket.data, never trust client-supplied identity fields.

Practice what you learned

Was this page helpful?

Topics covered

#Programming#SocketIOStudyNotes#BuildingAChatApplication#Building#Chat#Application#Socket#StudyNotes#SkillVeris#ExamPrep