WebSockets with Socket.io
Regular HTTP works in one direction at a time: the browser asks, the server answers, and the connection closes. WebSockets open a persistent, two-way connection. The server can push data to connected clients at any moment without the client asking first. This makes WebSockets the foundation of real-time features: live chat, notifications, collaborative editing, live dashboards, and multiplayer games. Socket.io is the most popular library for integrating WebSockets with Express.
HTTP vs WebSocket: The Difference
HTTP (request/response — like SMS): Client sends → Server replies → Connection closes Client sends → Server replies → Connection closes (Server can NEVER send first) WebSocket (persistent — like a phone call): Client connects → Connection stays open ← Server pushes message → Client sends message ← Server pushes message ← Server pushes message → Client sends message (Both sides can send at any time)
Real-World Use Cases
┌──────────────────────────────────────────────────────────────────┐ │ WebSocket Use Cases │ ├────────────────────────────┬─────────────────────────────────────┤ │ Live chat │ Messages appear instantly │ │ Notifications │ Alert users without page refresh │ │ Live sports scores │ Score updates pushed to viewers │ │ Collaborative documents │ Multiple users edit simultaneously │ │ Real-time dashboards │ Metrics update as they happen │ │ Online games │ Player positions sync in real-time │ │ Live order tracking │ Delivery status pushes to customer │ └────────────────────────────┴─────────────────────────────────────┘
Install Socket.io
npm install socket.io
Set Up Socket.io with Express
Socket.io wraps around Node's built-in HTTP server, which Express also uses:
const express = require('express');
const http = require('http');
const { Server } = require('socket.io');
const app = express();
const httpServer = http.createServer(app);
const io = new Server(httpServer, {
cors: {
origin: 'http://localhost:5173', // Your frontend URL
methods: ['GET', 'POST']
}
});
// Regular Express routes work normally
app.get('/', (req, res) => {
res.send('Socket.io server running');
});
// Socket.io connection handler
io.on('connection', (socket) => {
console.log('Client connected:', socket.id);
socket.on('disconnect', () => {
console.log('Client disconnected:', socket.id);
});
});
httpServer.listen(3000, () => {
console.log('Server running on port 3000');
});
Notice: httpServer.listen() instead of app.listen(). Socket.io needs direct access to the HTTP server.
Building a Live Chat App
Server-side: app.js
io.on('connection', (socket) => {
console.log(`User connected: ${socket.id}`);
// Listen for a user joining a room
socket.on('join_room', (roomName) => {
socket.join(roomName);
console.log(`${socket.id} joined room: ${roomName}`);
// Notify others in the room
socket.to(roomName).emit('user_joined', {
message: `A new user joined ${roomName}`
});
});
// Listen for a chat message
socket.on('send_message', (data) => {
const { room, message, username } = data;
// Broadcast the message to everyone in the room
io.to(room).emit('receive_message', {
username,
message,
timestamp: new Date().toISOString()
});
});
// Handle disconnection
socket.on('disconnect', () => {
console.log(`User disconnected: ${socket.id}`);
});
});
Client-side: browser JavaScript
<!-- Include Socket.io client from CDN -->
<script src="https://cdn.socket.io/4.6.0/socket.io.min.js"></script>
<script>
const socket = io('http://localhost:3000');
// Join a chat room
socket.emit('join_room', 'general');
// Send a message
function sendMessage(text) {
socket.emit('send_message', {
room: 'general',
username: 'Alice',
message: text
});
}
// Receive messages
socket.on('receive_message', (data) => {
console.log(`${data.username}: ${data.message}`);
// Add to your chat UI
});
// Someone joined
socket.on('user_joined', (data) => {
console.log(data.message);
});
</script>
Socket.io Event System
Socket.io uses a publish/subscribe event model:
EMIT = send an event
ON = listen for an event
┌─────────────────────────────────────────────────────────────────┐
│ Sending Events: Who Receives What │
├─────────────────────────────┬───────────────────────────────────┤
│ socket.emit('event', data) │ Send to THIS client only │
│ socket.broadcast.emit(...) │ Send to ALL clients except this │
│ io.emit('event', data) │ Send to ALL connected clients │
│ io.to(room).emit(...) │ Send to all clients in a room │
│ socket.to(room).emit(...) │ Room except this client │
└─────────────────────────────┴───────────────────────────────────┘
Rooms: Grouping Connections
Rooms let you broadcast to a subset of connected clients — perfect for chat channels, game lobbies, or per-user notification feeds:
io.on('connection', (socket) => {
// Join a room
socket.join('room-42');
// Leave a room
socket.leave('room-42');
// Broadcast to a specific room
io.to('room-42').emit('announcement', 'Hello everyone in room 42!');
// Get all sockets in a room
const sockets = await io.in('room-42').fetchSockets();
console.log(`${sockets.length} users in room 42`);
});
Sending Real-Time Notifications from a Route
Emit Socket.io events directly from Express routes — for example, notify all clients when a new product is added:
// Make io accessible in routes by attaching it to app
app.set('io', io);
// In your route
app.post('/api/products', async (req, res) => {
try {
const product = await Product.create(req.body);
// Notify all connected clients about the new product
const io = req.app.get('io');
io.emit('product_added', {
id: product.id,
name: product.name,
price: product.price
});
res.status(201).json({ data: product });
} catch (err) {
res.status(400).json({ error: err.message });
}
});
Connection Lifecycle Diagram
Browser Express + Socket.io Server
│ │
│── HTTP Upgrade request ────────────────→│
│ │ io.on('connection', socket => {})
│←─── WebSocket connection confirmed ─────│
│ │
│── socket.emit('join_room', 'general') → │
│ │ socket.on('join_room', ...)
│ │ socket.join('general')
│ │
│── socket.emit('send_message', {...}) ──→│
│ │ socket.on('send_message', ...)
│←─── io.to('general').emit(...) ─────────│
│ │
│── disconnects ────────────────────────→ │
│ │ socket.on('disconnect', ...)
Handling Authentication with Socket.io
Verify JWT tokens before accepting socket connections using Socket.io middleware:
const jwt = require('jsonwebtoken');
io.use((socket, next) => {
const token = socket.handshake.auth.token;
if (!token) {
return next(new Error('Authentication required'));
}
try {
const decoded = jwt.verify(token, process.env.JWT_SECRET);
socket.userId = decoded.id; // Attach user ID to socket
next();
} catch (err) {
next(new Error('Invalid token'));
}
});
// Client sends token on connection:
const socket = io('http://localhost:3000', {
auth: {
token: localStorage.getItem('authToken')
}
});
Summary
WebSockets maintain a persistent two-way connection between the browser and server, enabling the server to push data to clients without waiting for a request. Socket.io wraps WebSockets with a reliable event-based API and automatic fallback for older browsers. Create the server by wrapping Express in Node's http.createServer() and passing it to new Server(). Listen for connections with io.on('connection', socket => {}) and use custom event names with socket.on() and socket.emit(). Use rooms to broadcast to subsets of clients. Push Socket.io events from regular Express routes by attaching io to the app with app.set('io', io). Protect socket connections with JWT middleware using io.use().
