A live group chat room where messages appear instantly for all connected users — built with Node.js, Express, and Socket.io. Open in multiple browser tabs to test.
Create the project folder, initialise npm, and install the two packages you need.
mkdir chat-app && cd chat-app
npm init -y
npm install express socket.io
added 23 packages in 2s
The server serves the HTML page and handles WebSocket connections.
// 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);
app.get('/', (req, res) => {
res.sendFile(path.join(__dirname, 'index.html'));
});
// Track connected users
let userCount = 0;
io.on('connection', (socket) => {
userCount++;
console.log(`User connected — total: ${userCount}`);
// Tell everyone someone joined
io.emit('user-count', userCount);
io.emit('system-message', 'A new user joined the chat');
// Receive a message and broadcast to ALL users
socket.on('chat-message', (data) => {
io.emit('chat-message', data);
});
// Typing indicator — broadcast to everyone EXCEPT sender
socket.on('typing', (username) => {
socket.broadcast.emit('typing', username);
});
socket.on('stop-typing', () => {
socket.broadcast.emit('stop-typing');
});
socket.on('disconnect', () => {
userCount--;
io.emit('user-count', userCount);
io.emit('system-message', 'A user left the chat');
console.log(`User disconnected — total: ${userCount}`);
});
});
server.listen(3000, () => {
console.log('Chat server running at http://localhost:3000');
});
Create index.html with the message list, input form, and Socket.io client.
<!-- 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>
<style>
* { margin: 0; padding: 0; box-sizing: border-box; }
body { font-family: 'Segoe UI', sans-serif; background: #0f172a; color: #fff; height: 100vh; display: flex; flex-direction: column; }
.header { background: #1e293b; padding: 16px 20px; display: flex; justify-content: space-between; align-items: center; border-bottom: 1px solid #334155; }
.header h1 { font-size: 1.1rem; }
.user-count { font-size: 0.8rem; color: #22c55e; }
.messages { flex: 1; overflow-y: auto; padding: 20px; display: flex; flex-direction: column; gap: 10px; }
.msg { max-width: 70%; padding: 10px 14px; border-radius: 12px; font-size: 0.9rem; line-height: 1.4; }
.msg.mine { background: #1E3A6E; align-self: flex-end; border-bottom-right-radius: 4px; }
.msg.theirs{ background: #1e293b; align-self: flex-start; border-bottom-left-radius: 4px; }
.msg .name { font-size: 0.72rem; font-weight: 700; color: #F59C0D; margin-bottom: 3px; }
.msg.system{ background: transparent; color: #64748b; font-size: 0.78rem; text-align: center; align-self: center; font-style: italic; }
.typing-indicator { padding: 0 20px 8px; font-size: 0.78rem; color: #64748b; font-style: italic; min-height: 22px; }
.input-area { padding: 16px 20px; background: #1e293b; border-top: 1px solid #334155; display: flex; gap: 10px; }
.input-area input { flex: 1; padding: 10px 16px; border-radius: 24px; border: 1px solid #334155; background: #0f172a; color: #fff; font-size: 0.9rem; }
.input-area button { padding: 10px 20px; background: #F59C0D; border: none; border-radius: 24px; color: #fff; font-weight: 700; cursor: pointer; }
.username-screen { position: fixed; inset: 0; background: #0f172a; display: flex; align-items: center; justify-content: center; flex-direction: column; gap: 16px; z-index: 10; }
.username-screen h2 { font-size: 1.4rem; }
.username-screen input { padding: 12px 20px; border-radius: 10px; border: 1px solid #334155; background: #1e293b; color: #fff; font-size: 1rem; width: 280px; text-align: center; }
.username-screen button { padding: 12px 32px; background: #F59C0D; border: none; border-radius: 10px; color: #fff; font-weight: 700; font-size: 1rem; cursor: pointer; }
</style>
</head>
<body>
<?php require_once __DIR__ . '/../promo_banner.php'; ?>
<div class="username-screen" id="usernameScreen">
<h2>💬 Join Chat</h2>
<input type="text" id="usernameInput" placeholder="Your name..." maxlength="20" />
<button onclick="joinChat()">Join</button>
</div>
<div class="header">
<h1>💬 Chat Room</h1>
<span class="user-count" id="userCount">1 online</span>
</div>
<div class="messages" id="messages"></div>
<div class="typing-indicator" id="typingIndicator"></div>
<div class="input-area">
<input type="text" id="msgInput" placeholder="Type a message..." autocomplete="off" />
<button onclick="sendMessage()">Send</button>
</div>
<script src="/socket.io/socket.io.js"></script>
<script src="client.js"></script>
</body>
</html>
Connect to the server via Socket.io and handle sending, receiving, and typing events.
// client.js
const socket = io();
let username = '';
let typingTimer;
function joinChat() {
const input = document.getElementById('usernameInput').value.trim();
if (!input) return;
username = input;
document.getElementById('usernameScreen').style.display = 'none';
document.getElementById('msgInput').focus();
}
function sendMessage() {
const input = document.getElementById('msgInput');
const text = input.value.trim();
if (!text || !username) return;
socket.emit('chat-message', { username, text, time: new Date().toLocaleTimeString() });
socket.emit('stop-typing');
input.value = '';
}
// Send on Enter key
document.getElementById('msgInput').addEventListener('keypress', (e) => {
if (e.key === 'Enter') sendMessage();
});
// Typing indicator
document.getElementById('msgInput').addEventListener('input', () => {
if (!username) return;
socket.emit('typing', username);
clearTimeout(typingTimer);
typingTimer = setTimeout(() => socket.emit('stop-typing'), 1500);
});
// Receive messages
socket.on('chat-message', ({ username: sender, text, time }) => {
const messages = document.getElementById('messages');
const div = document.createElement('div');
div.className = `msg ${sender === username ? 'mine' : 'theirs'}`;
div.innerHTML = `<div class="name">${sender} · ${time}</div>${text}`;
messages.appendChild(div);
messages.scrollTop = messages.scrollHeight;
});
// System messages (join/leave)
socket.on('system-message', (text) => {
const messages = document.getElementById('messages');
const div = document.createElement('div');
div.className = 'msg system';
div.textContent = text;
messages.appendChild(div);
messages.scrollTop = messages.scrollHeight;
});
// User count
socket.on('user-count', (count) => {
document.getElementById('userCount').textContent = `${count} online`;
});
// Typing indicators
socket.on('typing', (name) => {
document.getElementById('typingIndicator').textContent = `${name} is typing...`;
});
socket.on('stop-typing', () => {
document.getElementById('typingIndicator').textContent = '';
});
# Start the server: node server.js # Output: Chat server running at http://localhost:3000 # Open http://localhost:3000 in two browser tabs # Enter different usernames in each tab # Type in one tab — messages appear instantly in the other
You built a real-time bidirectional app — the hardest concept in web development — in under 50 lines per file. Socket.io handles reconnections, fallbacks, and rooms automatically. Add io.to(roomName).emit() to create separate chat rooms.
Spotted a bug, broken code, or something that doesn't look right? Tell us what's off and we'll fix it.