← Back to Tutorials
JavaScript

Build a Chat Application with WebSockets

Difficulty: Intermediate Est. Time: ~3 hours

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

Continue Learning

Try these tutorials:

  • Build a REST API
  • Build a Markdown Editor
  • Build a Password Manager