Build a Chat Application with WebSockets
Introduction
Real-time chat applications are everywhere - from customer support widgets to team collaboration tools like Slack. In this tutorial, you'll build a complete chat application using WebSockets for real-time communication.
What You'll Build
- Real-time messaging with WebSockets
- Multiple chat rooms
- User presence indicators
- Typing indicators
- Message history
What You'll Learn
- WebSocket protocol fundamentals
- Socket.io library
- Real-time event handling
- Room management
- Broadcasting messages
Understanding WebSockets
WebSockets provide full-duplex communication between client and server.
HTTP vs WebSocket
- HTTP: Request-response, connection closes after each request
- WebSocket: Persistent connection, either side can send anytime
Real-Time Benefits
- Instant message delivery
- No page refresh needed
- Lower latency than polling
- Bi-directional communication
Project Overview
Our chat application will feature:
- Multiple chat rooms (General, Tech, Random)
- Real-time message delivery
- User join/leave notifications
- Online user list
- Clean, modern UI
Project Setup
Bash
# Create project
mkdir chat-app
cd chat-app
# Initialize
npm init -y
# Install dependencies
npm install express socket.io
Project Structure
Structure
chat-app/
├── public/
│ ├── index.html
│ ├── styles.css
│ └── app.js
├── server.js
└── package.json
Server Implementation
Let's build the WebSocket server.
JavaScript
// server.js
const express = require('express');
const http = require('http');
const { Server } = require('socket.io');
const path = require('path');
const app = express();
const server = http.createServer(app);
const io = new Server(server);
// Serve static files
app.use(express.static(path.join(__dirname, 'public')));
// Store users and rooms
const users = {}; // socket.id -> { username, room }
const rooms = ['general', 'tech', 'random'];
// Socket.io connection
io.on('connection', (socket) => {
console.log(`User connected: ${socket.id}`);
// Handle user joining
socket.on('joinRoom', ({ username, room }) => {
// Join the socket room
socket.join(room);
// Store user info
users[socket.id] = { username, room };
// Welcome message to user
socket.emit('message', formatMessage('Bot',
`Welcome to ${room}!`));
// Broadcast to others in room
socket.broadcast.to(room).emit('message',
formatMessage('Bot', `${username} has joined`));
// Send room and users info
io.to(room).emit('roomUsers', {
room: room,
users: getRoomUsers(room)
});
});
// Handle chat messages
socket.on('chatMessage', (msg) => {
const user = users[socket.id];
if (user) {
io.to(user.room).emit('message',
formatMessage(user.username, msg));
}
});
// Handle typing
socket.on('typing', () => {
const user = users[socket.id];
if (user) {
socket.broadcast.to(user.room).emit('typing', user.username);
}
});
// Handle disconnect
socket.on('disconnect', () => {
const user = users[socket.id];
if (user) {
io.to(user.room).emit('message',
formatMessage('Bot', `${user.username} has left`));
io.to(user.room).emit('roomUsers', {
room: user.room,
users: getRoomUsers(user.room)
});
delete users[socket.id];
}
});
});
// Helper functions
function formatMessage(username, text) {
return {
username,
text,
time: new Date().toLocaleTimeString()
};
}
function getRoomUsers(room) {
return Object.values(users)
.filter(user => user.room === room)
.map(user => user.username);
}
// Start server
const PORT = 3000;
server.listen(PORT, () => console.log(`Server on port ${PORT}`));
Client Implementation
Now let's build the frontend.
HTML
<!-- public/index.html -->
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Chat App</title>
<link rel="stylesheet" href="styles.css">
</head>
<body>
<!-- Join Screen -->
<div id="join-screen">
<h1>Join Chat</h1>
<form id="join-form">
<input type="text" id="username" placeholder="Username" required>
<select id="room">
<option value="general">General</option>
<option value="tech">Tech</option>
<option value="random">Random</option>
</select>
<button type="submit">Join</button>
</form>
</div>
<!-- Chat Screen -->
<div id="chat-screen" class="hidden">
<header>
<h1>Chat Room</h1>
<span id="room-name"></span>
</header>
<div class="chat-container">
<aside class="sidebar">
<h3>Users</h3>
<ul id="users"></ul>
</aside>
<main class="chat-main">
<div id="chat-messages"></div>
<div id="typing-indicator"></div>
</main>
</div>
<footer>
<form id="chat-form">
<input id="msg" type="text" placeholder="Message" required>
<button type="submit">Send</button>
</form>
</footer>
</div>
<script src="/socket.io/socket.io.js"></script>
<script src="app.js"></script>
</body>
</html>
JavaScript
// public/app.js
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('chat-messages');
const roomNameSpan = document.getElementById('room-name');
const usersList = document.getElementById('users');
const typingDiv = document.getElementById('typing-indicator');
let username = '';
let room = '';
// Join room
joinForm.addEventListener('submit', (e) => {
e.preventDefault();
username = document.getElementById('username').value;
room = document.getElementById('room'.value;
if (username && room) {
socket.emit('joinRoom', { username, room });
joinScreen.classList.add('hidden');
chatScreen.classList.remove('hidden');
}
});
// Send message
chatForm.addEventListener('submit', (e) => {
e.preventDefault();
const msg = document.getElementById('msg').value;
if (msg) {
socket.emit('chatMessage', msg);
document.getElementById('msg').value = '';
}
});
// Listen for messages
socket.on('message', (msg) => {
outputMessage(msg);
messagesDiv.scrollTop = messagesDiv.scrollHeight;
});
// Room users update
socket.on('roomUsers', ({ room, users }) => {
roomNameSpan.textContent = room;
usersList.innerHTML = users.map(user => `<li>${user}</li>`).join('');
});
// Typing indicator
socket.on('typing', (user) => {
typingDiv.textContent = `${user} is typing...`;
setTimeout(() => { typingDiv.textContent = ''; }, 3000);
});
// Output message to DOM
function outputMessage(msg) {
const div = document.createElement('div');
div.classList.add('message');
div.innerHTML = `
<p class="meta">${msg.username} <span>${msg.time}</span></p>
<p class="text">${msg.text}</p>
`;
messagesDiv.appendChild(div);
}
Adding Chat Rooms
Our server already supports multiple rooms. Users in different rooms don't see each other's messages.
Room Methods
- socket.join(room) - Join a room
- socket.leave(room) - Leave a room
- io.to(room).emit() - Message to room
- socket.broadcast.to(room).emit() - To room except sender
User Features
Let's add some polish with better styling.
CSS
/* public/styles.css */
* { margin: 0; padding: 0; box-sizing: border-box; }
body {
font-family: 'Segoe UI', sans-serif;
background: linear-gradient(135deg, #667eea, #764ba2);
height: 100vh;
}
.hidden { display: none !important; }
/* Join Screen */
#join-screen {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
height: 100vh;
}
#join-screen h1 { color: white; margin-bottom: 2rem; }
#join-form {
background: white;
padding: 2rem;
border-radius: 10px;
display: flex;
flex-direction: column;
gap: 1rem;
}
#join-form input, #join-form select {
padding: 0.75rem;
border: 1px solid #ddd;
border-radius: 5px;
}
#join-form button {
padding: 0.75rem;
background: #667eea;
color: white;
border: none;
border-radius: 5px;
cursor: pointer;
}
/* Chat Screen */
#chat-screen {
display: flex;
flex-direction: column;
height: 100vh;
}
header {
background: #333;
color: white;
padding: 1rem;
display: flex;
justify-content: space-between;
align-items: center;
}
.chat-container {
display: flex;
flex: 1;
overflow: hidden;
}
.sidebar {
width: 200px;
background: #f4f4f4;
padding: 1rem;
}
.sidebar ul { list-style: none; }
.sidebar li { padding: 0.5rem 0; }
.chat-main {
flex: 1;
display: flex;
flex-direction: column;
background: #fff;
}
#chat-messages {
flex: 1;
padding: 1rem;
overflow-y: auto;
}
.message {
background: #f4f4f4;
padding: 0.75rem;
border-radius: 8px;
margin-bottom: 1rem;
}
.message .meta {
font-size: 0.8rem;
color: #666;
margin-bottom: 0.25rem;
}
footer {
padding: 1rem;
background: #f4f4f4;
display: flex;
}
#chat-form {
display: flex;
gap: 0.5rem;
width: 100%;
}
#chat-form input {
flex: 1;
padding: 0.75rem;
border: 1px solid #ddd;
border-radius: 5px;
}
Enhancements
Ideas for extending the chat app:
- Private messaging between users
- Message read receipts
- Emoji support
- File/image sharing
- Message reactions
- User authentication
- Persistent message history
Summary
Congratulations! You've built a real-time chat application.
What You Built
- WebSocket Server - Express + Socket.io
- Chat Rooms - Multiple rooms support
- Real-time Messaging - Instant delivery
- User Presence - Online users list
- Typing Indicators - Real-time feedback
Next Steps
- Add database for message history
- Implement user authentication
- Add private messaging
- Deploy to production