Build a Real-time Chat App with Socket.io
Introduction
Real-time communication is at the heart of many modern web applications - from chat apps like Slack and Discord to live collaboration tools like Google Docs. In this tutorial, you'll build a fully functional real-time chat application from scratch.
By the end of this tutorial, you'll have created a chat app where users can join chat rooms, send messages instantly, see who's online, and communicate in real-time.
A real-time chat application with:
- Real-time messaging using WebSockets
- Multiple chat rooms
- Username selection
- Online user list
- Message history
- A clean, modern UI
- WebSocket communication basics
- Socket.io library for Node.js
- Real-time event handling
- Room management in Socket.io
- Building responsive frontends
What is WebSocket?
To understand WebSockets, let's first look at how traditional HTTP requests work.
The Problem with HTTP
HTTP is a request-response protocol. Here's how it works:
- Client sends a request to the server
- Server processes the request
- Server sends a response back to the client
- The connection closes
This works great for loading web pages, but it's problematic for chat. If you want to know if someone sent you a message, you'd have to constantly ask the server "Any new messages?" - this is called polling, and it's inefficient.
WebSockets to the Rescue
WebSocket is a different protocol that provides a persistent connection between the client and server. Once connected, either side can send messages at any time, without the other side asking.
Think of it like a phone call (WebSocket) vs. sending letters (HTTP):
- HTTP: You send a letter, wait for response, send another letter...
- WebSocket: You pick up the phone and can talk back and forth instantly
What is Socket.io?
Socket.io is a library that makes WebSockets even easier to use. It provides:
- Automatic reconnection
- Room support for group chats
- Event-based communication
- FallBack to HTTP long-polling if WebSocket isn't available
- Works across all browsers
Project Overview
Our chat application will have the following features:
Core Features
- Username System - Users pick a username when they join
- Chat Rooms - Multiple rooms for different topics
- Real-time Messages - Messages appear instantly for everyone in the room
- User List - See who's currently in each room
- System Messages - Notifications when users join/leave
Technical Architecture
- Backend: Node.js with Express
- Real-time: Socket.io
- Frontend: HTML, CSS, Vanilla JavaScript
Here's what happens when you send a message:
- You type a message in the chat box
- JavaScript emits a 'chat message' event to the server
- Server receives the event
- Server broadcasts the message to everyone in the room
- All clients receive and display the message
Prerequisites
Before starting this tutorial, ensure you have:
- Node.js installed - Download from nodejs.org (version 14 or higher)
- Code editor - VS Code recommended
- Basic JavaScript knowledge - Variables, functions, objects
- Basic understanding of HTML/CSS
You can verify Node.js is installed by running:
node --version
Setting Up the Project
Let's set up our Node.js project and install the necessary packages.
Creating the Project
# Create project directory
mkdir chat-app
cd chat-app
# Initialize Node.js project
npm init -y
# Install dependencies
npm install express socket.io
# What each package does:
# express - Web server framework
# socket.io - Real-time communication library
Project Structure
chat-app/
├── public/
│ ├── index.html # Main HTML file
│ ├── style.css # Styling
│ └── client.js # Client-side JavaScript
├── server.js # Server code
└── package.json # Dependencies
Building the Server
The server is the heart of our chat application. It handles WebSocket connections, manages rooms, and relays messages between users.
// server.js
const express = require('express');
const http = require('http');
const { Server } = require("socket.io");
const path = require('path');
// Create Express app and HTTP server
const app = express();
const server = http.createServer(app);
const io = new Server(server);
// Serve static files from public directory
app.use(express.static(path.join(__dirname, 'public')));
// Store users in each room
const users = {};
// Socket.io connection handler
io.on('connection', (socket) => => {
console.log('A user connected:', socket.id);
// Handle user joining
socket.on('joinRoom', ({ username, room }) => => {
// Join the socket to the room
socket.join(room);
// Store user info
users[socket.id] = { username, room };
// Welcome message to the user
socket.emit('message', formatMessage('System',
`Welcome to ${room}!`));
// Broadcast to others in the room that a user joined
socket.broadcast.to(room).emit('message',
formatMessage('System', `${username} has joined the chat`));
// Send room info to the user
socket.emit('roomInfo', {
room: room,
users: getRoomUsers(room)
});
});
// Handle chat messages
socket.on('chatMessage', (msg) => {
const user = users[socket.id];
if (user) {
// Send message to everyone in the room
io.to(user.room).emit('message',
formatMessage(user.username, msg));
}
});
// Handle user disconnecting
socket.on('disconnect', () => {
const user = users[socket.id];
if (user) {
// Tell everyone in the room that user left
io.to(user.room).emit('message',
formatMessage('System', `${user.username} has left the chat`));
// Update the user list
io.to(user.room).emit('roomInfo', {
room: user.room,
users: getRoomUsers(user.room)
});
// Remove user from our tracking
delete users[socket.id];
}
});
});
// Helper function to format messages
function formatMessage(username, text) {
return {
username,
text,
time: new Date().toLocaleTimeString()
};
}
// Get all users in a room
function getRoomUsers(room) {
return Object.values(users)
.filter(user => user.room === room)
.map(user => user.username);
}
// Start the server
const PORT = 3000;
server.listen(PORT, () => {
console.log(`Server running on port ${PORT}`);
});
Here are the key Socket.io methods used:
- socket.on(event, callback) - Listen for an event
- socket.emit(event, data) - Send to the connected client
- socket.broadcast.to(room).emit() - Send to everyone in a room except sender
- io.to(room).emit() - Send to everyone in a room
- socket.join(room) - Add socket to a room
Creating the Frontend HTML
Now let's create the HTML structure for our chat interface.
<!-- public/index.html -->
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Chat App</title>
<link rel="stylesheet" href="style.css">
</head>
<body>
<!-- Join Screen -->
<div id="join-screen">
<h1>Join Chat</h1>
<form id="join-form">
<input type="text" id="username" placeholder="Enter username..." required>
<select id="room">
<option value="general">General</option>
<option value="tech">Tech Talk</option>
<option value="gaming">Gaming</option>
<option value="music">Music</option>
</select>
<button type="submit">Join Room</button>
</form>
</div>
<!-- Chat Screen (hidden initially) -->
<div id="chat-screen" class="hidden">
<header>
<h1>Chat Room</h1>
<span id="room-name"></span>
<button id="leave-btn">Leave Room</button>
</header>
<main>
<div id="messages"></div>
</main>
<footer>
<form id="chat-form">
<input id="msg" type="text" placeholder="Type a message..." required>
<button type="submit">Send</button>
</form>
</footer>
</div>
<!-- Socket.io Client Library -->
<script src="/socket.io/socket.io.js"></script>
<script src="client.js"></script>
</body>
</html>
Socket.io automatically serves the client library at /socket.io/socket.io.js. You don't need to download anything - just include this script tag!
Client-Side JavaScript
Now let's add the client-side JavaScript to handle sending and receiving messages.
// public/client.js
// Initialize Socket.io client
const socket = io();
// DOM Elements
const joinForm = document.getElementById('join-form');
const chatForm = document.getElementById('chat-form');
const joinScreen = document.getElementById('join-screen');
const chatScreen = document.getElementById('chat-screen');
const messagesDiv = document.getElementById('messages');
const roomNameSpan = document.getElementById('room-name');
const leaveBtn = document.getElementById('leave-btn');
let username = '';
let room = '';
// Join room when form is submitted
joinForm.addEventListener('submit', (e) => {
e.preventDefault();
username = document.getElementById('username').value;
room = document.getElementById('room'.value;
if (username && room) {
// Emit joinRoom event to server
socket.emit('joinRoom', { username, room });
// Show chat screen, hide join screen
joinScreen.classList.add('hidden');
chatScreen.classList.remove('hidden');
}
});
// Send message when form is submitted
chatForm.addEventListener('submit', (e) => {
e.preventDefault();
const msgInput = document.getElementById('msg');
const message = msgInput.value;
// Don't send empty messages
if (!message) return;
// Emit chatMessage event to server
socket.emit('chatMessage', message);
// Clear input
msgInput.value = '';
msgInput.focus();
});
// Leave room button
leaveBtn.addEventListener('click', () => {
window.location.reload();
});
// Listen for messages from server
socket.on('message', (message) => {
outputMessage(message);
// Auto-scroll to bottom
messagesDiv.scrollTop = messagesDiv.scrollHeight;
});
// Listen for room info
socket.on('roomInfo', ({ room }) => {
roomNameSpan.textContent = room;
});
// Display message in DOM
function outputMessage(message) {
const div = document.createElement('div');
// Check if it's a system message
if (message.username === 'System') {
div.classList.add('system-message');
div.innerHTML = `<p>${message.text}</p>`;
} else {
div.classList.add('message');
div.innerHTML = `
<p class="meta">${message.username} <span>${message.time}</span></p>
<p class="text">${message.text}</p>
`;
}
messagesDiv.appendChild(div);
}
Here's how communication works:
- User submits message form
- Client emits 'chatMessage' event with the message text
- Server receives the event and broadcasts to everyone in the room
- All clients receive 'message' event
- Each client displays the message in their chat window
Implementing Chat Rooms
Chat rooms allow multiple groups to chat independently. Socket.io makes this easy with the join() method.
How Rooms Work
In Socket.io, each socket connection can "join" one or more rooms. When you emit to a room, only sockets in that room receive the message.
// Server-side: Joining rooms
// When a user joins
socket.on('joinRoom', ({ username, room }) => => {
// Add socket to the room
socket.join(room);
// Now any message sent to 'room' goes to everyone in it
io.to(room).emit('message', `${username} joined!`);
});
// When a user sends a message
socket.on('chatMessage', (msg) => => {
// Get user's room from our data
const userRoom = users[socket.id].room;
// Send to everyone in that specific room
io.to(userRoom).emit('message', message);
});
// When a user leaves (disconnects)
socket.on('disconnect', () => => {
// Get user's room before removing
const userRoom = users[socket.id].room;
// Notify everyone in that room
io.to(userRoom).emit('message', `${username} left!`);
});
Socket.io provides two ways to emit:
- io.emit() - Send to everyone (all rooms)
- io.to('roomName').emit() - Send to specific room only
- socket.broadcast.emit() - Send to everyone except sender
- socket.broadcast.to('roomName').emit() - Send to room except sender
Private Messaging (Bonus Feature)
Once you have the basic chat working, here's how to add private messaging between users.
// Server - Add private message handling
// Listen for private message events
socket.on('privateMessage', ({ to, message }) => => {
// Find the socket ID of the recipient
const recipientSocket = findSocketByUsername(to);
if (recipientSocket) {
// Send to specific user
io.to(recipientSocket).emit('privateMessage', {
from: users[socket.id].username,
message: message,
time: new Date().toLocaleTimeString()
});
// Also confirm to sender
socket.emit('privateMessage', {
from: 'You',
to: to,
message: message,
time: new Date().toLocaleTimeString()
});
}
});
// Helper to find socket ID by username
function findSocketByUsername(username) {
for (const [socketId, user] of Object.entries(users)) {
if (user.username === username) return socketId;
}
return null;
}
In a production app, you should validate that users can only send private messages to people in the same room, and add authentication to prevent impersonation.
Testing the Application
Let's test our chat application to make sure everything works.
Starting the Server
# Run the server
node server.js
# You should see:
# Server running on port 3000
Testing Steps
- Open your browser to
http://localhost:3000 - Open a second browser tab (or incognito window)
- Enter different usernames but the same room
- Send messages between the tabs
- Try a different room in a third tab
- ✓ User can join a room with username
- ✓ Messages appear instantly for all users in the room
- ✓ Users in different rooms don't see each other's messages
- ✓ System messages show when users join/leave
- ✓ Chat history scrolls properly
Deployment
When you're ready to deploy your chat app, here are your options.
Platform Options
- Render - Free tier, good for Node.js
- Railway - Easy deployment, reasonable free tier
- Glitch - Quick prototyping, great for learning
- Heroku - Industry standard, credit card required
Important Considerations
Most hosting platforms support WebSockets, but some require configuration:
- Render/Railway - Works out of the box
- Heroku - May need websockets addon
- Glitch - Works with some limitations
Make sure your platform allows long-running connections, as WebSocket connections stay open!
Summary
Congratulations! You've built a real-time chat application from scratch.
What You Built
- Express Server - Serves static files and handles WebSocket connections
- Socket.io Integration - Enables real-time bidirectional communication
- Chat Rooms - Allows multiple topics/groups
- User Tracking - Knows who's in each room
- Message Broadcasting - Messages go to the right people
Key Concepts Learned
- WebSocket vs HTTP
- Socket.io events and broadcasting
- Room management
- Real-time UI updates
Next Steps
Try adding these features:
- Typing indicators
- Message read receipts
- Online/offline status
- File sharing
- Emoji support
- Message reactions