Episode 3 — NodeJS MongoDB Backend Architecture / 3.15 — Realtime Communication WebSockets
3.15.c — Socket.io Setup and Basics
Socket.io is a JavaScript library for real-time, bidirectional, event-based communication. It provides a simple API on top of WebSocket with automatic fallbacks, reconnection, rooms, namespaces, and broadcasting out of the box.
<< Previous: 3.15.b — HTTP Polling & Alternatives | Next: 3.15.d — Rooms & Namespaces >>
1. What is Socket.io?
Socket.io consists of two parts:
- Server library (
socket.io) — runs on Node.js - Client library (
socket.io-client) — runs in the browser or Node.js
It abstracts away the complexity of WebSocket management and provides a high-level, event-driven API.
┌─────────────────┐ ┌──────────────────┐
│ Browser Client │ ══════> │ Express + Socket │
│ socket.io-client│ <══════ │ .io Server │
└─────────────────┘ └──────────────────┘
emit('chat', msg) on('chat', callback)
on('response', cb) emit('response', data)
2. Socket.io vs Raw WebSocket
| Feature | Raw WebSocket | Socket.io |
|---|---|---|
| Reconnection | Manual | Automatic with backoff |
| Fallback transports | None | Long polling, then upgrade |
| Event-based API | onmessage (single handler) | on('eventName') (multiple) |
| Rooms | None | Built-in |
| Namespaces | None | Built-in |
| Broadcasting | Manual to each client | io.emit(), socket.broadcast.emit() |
| Binary support | Manual | Automatic detection |
| Acknowledgements | None | Callback-based acks |
| Middleware | None | io.use() |
| Packet size | Smaller | Slightly larger (protocol overhead) |
3. Installation
# Server-side
npm install socket.io
# Client-side (for frontend frameworks or Node.js clients)
npm install socket.io-client
# For plain HTML, use CDN:
# <script src="https://cdn.socket.io/4.7.4/socket.io.min.js"></script>
4. Setting Up Socket.io with Express
Basic Setup
// server.js
const express = require('express');
const http = require('http');
const { Server } = require('socket.io');
const app = express();
// IMPORTANT: Create HTTP server from Express app
// Socket.io needs the raw HTTP server, not the Express app
const server = http.createServer(app);
// Attach Socket.io to the HTTP server
const io = new Server(server);
// Serve static files (for the client)
app.use(express.static('public'));
// REST API routes still work normally
app.get('/api/health', (req, res) => {
res.json({ status: 'ok' });
});
// Socket.io connection handler
io.on('connection', (socket) => {
console.log(`User connected: ${socket.id}`);
socket.on('disconnect', () => {
console.log(`User disconnected: ${socket.id}`);
});
});
// IMPORTANT: Use server.listen(), NOT app.listen()
const PORT = 3000;
server.listen(PORT, () => {
console.log(`Server running on http://localhost:${PORT}`);
});
Common mistake: Using app.listen() instead of server.listen(). The app.listen() method creates its own HTTP server internally, which Socket.io will not be attached to.
5. CORS Configuration
When your frontend and backend run on different origins (e.g., React on port 5173 and Express on port 3000), you need CORS configuration:
const io = new Server(server, {
cors: {
origin: 'http://localhost:5173', // Frontend URL
// origin: ['http://localhost:5173', 'https://myapp.com'], // Multiple origins
// origin: '*', // All origins (dev only!)
methods: ['GET', 'POST'],
credentials: true // Allow cookies/auth headers
}
});
6. Client Connection
In an HTML File
<!-- public/index.html -->
<!DOCTYPE html>
<html>
<head>
<title>Socket.io Chat</title>
</head>
<body>
<h1>Chat App</h1>
<!-- Socket.io auto-serves its client library at this path -->
<script src="/socket.io/socket.io.js"></script>
<script>
// Connect to the server (same origin)
const socket = io();
// Or connect to a specific URL
// const socket = io('http://localhost:3000');
socket.on('connect', () => {
console.log('Connected! My ID:', socket.id);
});
</script>
</body>
</html>
In a React/Vite Project
// src/socket.js
import { io } from 'socket.io-client';
const SERVER_URL = 'http://localhost:3000';
export const socket = io(SERVER_URL, {
autoConnect: false, // Don't connect until we explicitly call connect()
auth: {
token: localStorage.getItem('jwt') // Send auth token on connection
}
});
// src/App.jsx
import { useEffect, useState } from 'react';
import { socket } from './socket';
function App() {
const [isConnected, setIsConnected] = useState(false);
useEffect(() => {
socket.connect();
socket.on('connect', () => setIsConnected(true));
socket.on('disconnect', () => setIsConnected(false));
// Cleanup on unmount
return () => {
socket.off('connect');
socket.off('disconnect');
socket.disconnect();
};
}, []);
return <div>Status: {isConnected ? 'Connected' : 'Disconnected'}</div>;
}
7. Core Events and API
Connection Events (Server-Side)
io.on('connection', (socket) => {
// socket.id — unique identifier for this connection
console.log('New connection:', socket.id);
// socket.handshake — info about the initial handshake
console.log('Headers:', socket.handshake.headers);
console.log('Query:', socket.handshake.query);
console.log('Auth:', socket.handshake.auth);
// DISCONNECT — client disconnected
socket.on('disconnect', (reason) => {
console.log(`Disconnected: ${socket.id}, Reason: ${reason}`);
// Reasons: 'io server disconnect', 'io client disconnect',
// 'ping timeout', 'transport close', 'transport error'
});
// ERROR
socket.on('error', (error) => {
console.error('Socket error:', error);
});
});
Emitting and Listening to Custom Events
// ========== SERVER ==========
io.on('connection', (socket) => {
// LISTENING for events from this client
socket.on('chat-message', (data) => {
console.log(`${data.user}: ${data.text}`);
// Process the message, then broadcast to others
socket.broadcast.emit('chat-message', {
user: data.user,
text: data.text,
timestamp: Date.now()
});
});
// EMITTING an event to this specific client
socket.emit('welcome', {
message: 'Welcome to the chat!',
onlineUsers: getOnlineUserCount()
});
// Listening for typing indicator
socket.on('typing', (username) => {
socket.broadcast.emit('user-typing', username);
});
socket.on('stop-typing', (username) => {
socket.broadcast.emit('user-stopped-typing', username);
});
});
// ========== CLIENT ==========
const socket = io();
// EMIT event to server
socket.emit('chat-message', {
user: 'Alice',
text: 'Hello everyone!'
});
// LISTEN for events from server
socket.on('welcome', (data) => {
console.log(data.message); // "Welcome to the chat!"
console.log(data.onlineUsers); // 5
});
socket.on('chat-message', (data) => {
displayMessage(data.user, data.text, data.timestamp);
});
// Typing indicator
inputField.addEventListener('input', () => {
socket.emit('typing', currentUser.name);
clearTimeout(typingTimeout);
typingTimeout = setTimeout(() => {
socket.emit('stop-typing', currentUser.name);
}, 2000);
});
8. Broadcasting Patterns
io.on('connection', (socket) => {
// 1. Send to THIS client only
socket.emit('private', 'Only you get this');
// 2. Send to ALL connected clients (including sender)
io.emit('announcement', 'Server is restarting in 5 minutes');
// 3. Send to ALL clients EXCEPT the sender
socket.broadcast.emit('user-joined', `${socket.id} joined the chat`);
// 4. Send to a specific ROOM (all members, including sender)
io.to('room-name').emit('room-event', data);
// 5. Send to a specific ROOM (excluding sender)
socket.to('room-name').emit('room-event', data);
// 6. Send to multiple rooms
io.to('room1').to('room2').emit('multi-room', data);
// 7. Send to a specific socket by ID
io.to(targetSocketId).emit('direct-message', data);
});
9. Acknowledgements (Callbacks)
Socket.io supports acknowledgements -- the receiver can respond to confirm receipt:
// ========== CLIENT ==========
socket.emit('save-message', { text: 'Hello' }, (response) => {
// This callback fires when the server acknowledges
if (response.status === 'ok') {
console.log('Message saved! ID:', response.id);
} else {
console.error('Failed to save:', response.error);
}
});
// ========== SERVER ==========
socket.on('save-message', async (data, callback) => {
try {
const message = await Message.create({ text: data.text });
callback({ status: 'ok', id: message._id });
} catch (error) {
callback({ status: 'error', error: error.message });
}
});
10. Complete Chat Application Example
Server
// server.js
const express = require('express');
const http = require('http');
const { Server } = require('socket.io');
const app = express();
const server = http.createServer(app);
const io = new Server(server, {
cors: { origin: '*' }
});
app.use(express.static('public'));
// Track online users
const onlineUsers = new Map(); // socketId -> { username, joinedAt }
io.on('connection', (socket) => {
console.log(`Connected: ${socket.id}`);
// User joins the chat
socket.on('user-join', (username) => {
onlineUsers.set(socket.id, {
username,
joinedAt: new Date()
});
// Notify everyone
io.emit('user-list', Array.from(onlineUsers.values()));
socket.broadcast.emit('system-message', {
text: `${username} joined the chat`,
timestamp: Date.now()
});
});
// Handle chat messages
socket.on('chat-message', (data, callback) => {
const user = onlineUsers.get(socket.id);
if (!user) return callback?.({ error: 'Not authenticated' });
const message = {
id: Date.now(),
user: user.username,
text: data.text,
timestamp: Date.now()
};
// Broadcast to all (including sender, so they see it in the feed)
io.emit('chat-message', message);
callback?.({ status: 'ok', id: message.id });
});
// Typing indicator
socket.on('typing', () => {
const user = onlineUsers.get(socket.id);
if (user) {
socket.broadcast.emit('user-typing', user.username);
}
});
socket.on('stop-typing', () => {
const user = onlineUsers.get(socket.id);
if (user) {
socket.broadcast.emit('user-stopped-typing', user.username);
}
});
// Handle disconnect
socket.on('disconnect', () => {
const user = onlineUsers.get(socket.id);
if (user) {
onlineUsers.delete(socket.id);
io.emit('user-list', Array.from(onlineUsers.values()));
io.emit('system-message', {
text: `${user.username} left the chat`,
timestamp: Date.now()
});
}
});
});
server.listen(3000, () => {
console.log('Chat server running on http://localhost:3000');
});
Client
<!-- public/index.html -->
<!DOCTYPE html>
<html>
<head>
<title>Socket.io Chat</title>
<style>
#messages { list-style: none; padding: 0; max-height: 400px; overflow-y: auto; }
#messages li { padding: 5px 10px; border-bottom: 1px solid #eee; }
.system { color: #999; font-style: italic; }
#typing { color: #666; height: 20px; }
</style>
</head>
<body>
<div id="login">
<input id="username" placeholder="Enter your name" />
<button onclick="joinChat()">Join Chat</button>
</div>
<div id="chat" style="display:none">
<h3>Online: <span id="online-count">0</span></h3>
<ul id="messages"></ul>
<p id="typing"></p>
<input id="message-input" placeholder="Type a message..." />
<button onclick="sendMessage()">Send</button>
</div>
<script src="/socket.io/socket.io.js"></script>
<script>
const socket = io();
let typingTimeout;
function joinChat() {
const username = document.getElementById('username').value.trim();
if (!username) return alert('Enter a name!');
socket.emit('user-join', username);
document.getElementById('login').style.display = 'none';
document.getElementById('chat').style.display = 'block';
document.getElementById('message-input').focus();
}
function sendMessage() {
const input = document.getElementById('message-input');
const text = input.value.trim();
if (!text) return;
socket.emit('chat-message', { text }, (response) => {
if (response.error) alert(response.error);
});
input.value = '';
socket.emit('stop-typing');
}
// Listen for messages
socket.on('chat-message', (msg) => {
const li = document.createElement('li');
li.textContent = `${msg.user}: ${msg.text}`;
document.getElementById('messages').appendChild(li);
});
socket.on('system-message', (msg) => {
const li = document.createElement('li');
li.className = 'system';
li.textContent = msg.text;
document.getElementById('messages').appendChild(li);
});
socket.on('user-list', (users) => {
document.getElementById('online-count').textContent = users.length;
});
// Typing indicator
document.getElementById('message-input')?.addEventListener('keyup', (e) => {
if (e.key === 'Enter') return sendMessage();
socket.emit('typing');
clearTimeout(typingTimeout);
typingTimeout = setTimeout(() => socket.emit('stop-typing'), 2000);
});
socket.on('user-typing', (username) => {
document.getElementById('typing').textContent = `${username} is typing...`;
});
socket.on('user-stopped-typing', () => {
document.getElementById('typing').textContent = '';
});
</script>
</body>
</html>
Key Takeaways
- Socket.io has two parts: server (
socket.io) and client (socket.io-client) - Always create an
http.createServer(app)and pass it to Socket.io -- do not useapp.listen() - Communication is event-based:
emit('event', data)to send,on('event', callback)to receive - Broadcasting sends to all or selected clients:
io.emit(),socket.broadcast.emit() - Acknowledgements let the receiver respond to confirm receipt via callbacks
- Configure CORS when frontend and backend are on different origins
- Socket.io auto-serves its client library at
/socket.io/socket.io.js
Explain-It Challenge
Scenario: Your team built the chat app above, but users report that messages sometimes appear duplicated. After debugging, you notice that the useEffect in the React component runs twice (React Strict Mode).
Explain why this causes duplicate messages, what the root cause is, and how you would fix it. Hint: Think about event listener cleanup and the Socket.io off() method.
<< Previous: 3.15.b — HTTP Polling & Alternatives | Next: 3.15.d — Rooms & Namespaces >>