Building a Real Time Chat Application
From Concepts to a Working Chat App
The best way to solidify Socket.IO knowledge is to build a complete feature. This lesson walks through a real-time chat application with usernames, multiple chat rooms, typing indicators, and disconnect handling.
Server: Full Setup
npm install express socket.io
// server.js
const express = require('express');
const { createServer } = require('http');
const { Server } = require('socket.io');
const path = require('path');
const app = express();
const server = createServer(app);
const io = new Server(server);
// Serve the static HTML client
app.use(express.static(path.join(__dirname, 'public')));
// In-memory message history (replace with MongoDB for persistence)
const messageHistory = [];
const MAX_HISTORY = 50;
io.on('connection', (socket) => {
let currentUser = null;
let currentRoom = 'general';
// 1. User joins with a display name
socket.on('user-join', ({ username, room = 'general' }) => {
currentUser = username;
currentRoom = room;
socket.join(room);
// Send message history to the new user only
socket.emit('message-history', messageHistory.filter(m => m.room === room));
// Notify everyone else in the room
socket.to(room).emit('system-message', {
text: `${username} has joined the chat`,
timestamp: new Date().toISOString(),
});
// Update the online user list for everyone in the room
const roomSockets = io.sockets.adapter.rooms.get(room);
io.to(room).emit('user-count', roomSockets ? roomSockets.size : 0);
console.log(`${username} joined room: ${room}`);
});
// 2. User sends a message
socket.on('send-message', ({ text }) => {
if (!currentUser || !text.trim()) return;
const message = {
id: Date.now(),
username: currentUser,
text: text.trim(),
room: currentRoom,
timestamp: new Date().toISOString(),
};
// Store in memory
messageHistory.push(message);
if (messageHistory.length > MAX_HISTORY) messageHistory.shift();
// Broadcast to everyone in the room (including sender)
io.to(currentRoom).emit('new-message', message);
});
// 3. Typing indicator
socket.on('typing-start', () => {
if (currentUser) {
socket.to(currentRoom).emit('user-typing', { username: currentUser });
}
});
socket.on('typing-stop', () => {
if (currentUser) {
socket.to(currentRoom).emit('user-stopped-typing', { username: currentUser });
}
});
// 4. Switch rooms
socket.on('join-room', ({ room }) => {
socket.leave(currentRoom);
socket.to(currentRoom).emit('system-message', {
text: `${currentUser} has left`,
timestamp: new Date().toISOString(),
});
currentRoom = room;
socket.join(room);
socket.emit('message-history', messageHistory.filter(m => m.room === room));
socket.to(room).emit('system-message', {
text: `${currentUser} has joined`,
timestamp: new Date().toISOString(),
});
});
// 5. Disconnect
socket.on('disconnect', () => {
if (currentUser) {
socket.to(currentRoom).emit('system-message', {
text: `${currentUser} has left the chat`,
timestamp: new Date().toISOString(),
});
const roomSockets = io.sockets.adapter.rooms.get(currentRoom);
io.to(currentRoom).emit('user-count', roomSockets ? roomSockets.size : 0);
console.log(`${currentUser} disconnected`);
}
});
});
server.listen(5000, () => {
console.log('Chat server running on http://localhost:5000');
});
Client: HTML + Socket.IO
<!-- public/index.html -->
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<title>CareerEx Chat</title>
</head>
<body>
<div id="login">
<input id="usernameInput" placeholder="Enter your name" />
<button onclick="joinChat()">Join Chat</button>
</div>
<div id="chat" style="display:none">
<div id="messages"></div>
<div id="typing-indicator"></div>
<div id="input-area">
<input id="messageInput" placeholder="Type a message..." />
<button onclick="sendMessage()">Send</button>
</div>
</div>
<script src="/socket.io/socket.io.js"></script>
<script>
const socket = io();
let typingTimer;
function joinChat() {
const username = document.getElementById('usernameInput').value.trim();
if (!username) return;
socket.emit('user-join', { username, room: 'general' });
document.getElementById('login').style.display = 'none';
document.getElementById('chat').style.display = 'block';
}
function sendMessage() {
const input = document.getElementById('messageInput');
const text = input.value.trim();
if (!text) return;
socket.emit('send-message', { text });
socket.emit('typing-stop');
input.value = '';
}
// Handle typing indicator
document.getElementById('messageInput').addEventListener('input', () => {
socket.emit('typing-start');
clearTimeout(typingTimer);
typingTimer = setTimeout(() => socket.emit('typing-stop'), 1500);
});
// Receive messages
socket.on('new-message', (message) => {
addMessage(message.username + ': ' + message.text, 'user');
});
socket.on('message-history', (messages) => {
messages.forEach(m => addMessage(m.username + ': ' + m.text, 'user'));
});
socket.on('system-message', (data) => {
addMessage(data.text, 'system');
});
socket.on('user-typing', ({ username }) => {
document.getElementById('typing-indicator').textContent = username + ' is typing...';
});
socket.on('user-stopped-typing', () => {
document.getElementById('typing-indicator').textContent = '';
});
function addMessage(text, type) {
const div = document.createElement('div');
div.className = 'message ' + type;
div.textContent = text;
const msgs = document.getElementById('messages');
msgs.appendChild(div);
msgs.scrollTop = msgs.scrollHeight;
}
</script>
</body>
</html>
Persisting Messages to MongoDB
In-memory storage is lost when the server restarts. For production, save messages to MongoDB:
const mongoose = require('mongoose');
const messageSchema = new mongoose.Schema({
username: { type: String, required: true },
text: { type: String, required: true },
room: { type: String, default: 'general' },
createdAt: { type: Date, default: Date.now },
});
const Message = mongoose.model('Message', messageSchema);
// In the send-message handler:
socket.on('send-message', async ({ text }) => {
const saved = await Message.create({ username: currentUser, text, room: currentRoom });
io.to(currentRoom).emit('new-message', saved);
});
// When a user joins, fetch the last 50 messages from MongoDB:
socket.on('user-join', async ({ username, room }) => {
const history = await Message.find({ room }).sort({ createdAt: -1 }).limit(50);
socket.emit('message-history', history.reverse());
});
Practice Exercise
- Build the complete chat server shown above.
- Create a second room called "tech-questions" and allow users to switch between "general" and "tech-questions".
- Add a
/nick newnamecommand that lets a user change their display name mid-session. - Display the count of users currently in the room in the UI.
- (Bonus) Persist messages to MongoDB and load history when joining a room.
Try it yourself
Key Takeaways
- Track each connected socket's username and current room in local variables inside the connection handler.
- Emit message history only to the joining socket with socket.emit; broadcast system messages (joined/left) with socket.to(room).emit.
- Typing indicators use a separate lightweight event and a client-side timeout to clear them after inactivity.
- In-memory message history is simple but lost on restart; persist to MongoDB for production durability.
- Handle the disconnect event to notify remaining room members and update the online user count.
Quick Quiz
1.Why should you limit the in-memory message history (e.g. MAX_HISTORY = 50) on the server?
2.What event should the server emit to the joining user (only) when they enter a room?
3.How does the typing indicator work in Socket.IO without broadcasting the actual message?
4.What is the advantage of persisting chat messages in MongoDB over keeping them in memory?
Ready to go further?
CareerEx gives you structured 12-week training, live classes every Saturday and Sunday, real tutor feedback, and a certificate. Join the next cohort.
Join CareerEx