← Back to Tutorials
Node.js

Build a Real-time Chat App with Socket.io

Difficulty: Intermediate Est. Time: ~4 hours

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.

What You'll Build

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
What You'll Learn
  • 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:

  1. Client sends a request to the server
  2. Server processes the request
  3. Server sends a response back to the client
  4. 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.

How WebSockets Work

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
How Messages Flow

Here's what happens when you send a message:

  1. You type a message in the chat box
  2. JavaScript emits a 'chat message' event to the server
  3. Server receives the event
  4. Server broadcasts the message to everyone in the room
  5. 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:

Bash
node --version

Setting Up the Project

Let's set up our Node.js project and install the necessary packages.

Creating the Project

Bash
# 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

File 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.

JavaScript
// 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}`);
});
Understanding Socket.io Events

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.

HTML
<!-- 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>
Important Note

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.

JavaScript
// 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);
}
Event Flow

Here's how communication works:

  1. User submits message form
  2. Client emits 'chatMessage' event with the message text
  3. Server receives the event and broadcasts to everyone in the room
  4. All clients receive 'message' event
  5. 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.

JavaScript
// 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!`);
});
Room vs Global

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.

JavaScript
// 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;
}
Security Consideration

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

Bash
# Run the server
node server.js

# You should see:
# Server running on port 3000

Testing Steps

  1. Open your browser to http://localhost:3000
  2. Open a second browser tab (or incognito window)
  3. Enter different usernames but the same room
  4. Send messages between the tabs
  5. Try a different room in a third tab
Testing Checklist
  • ✓ 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

WebSocket in Production

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

Continue Learning

Ready for more? Try these tutorials:

  • Build a Blog CMS - Full-stack web development
  • Build a URL Shortener - API design
  • Build a Task Manager - Frontend state management