HTTP request/response is great for typical CRUD APIs, but real-time features, chat, live notifications, dashboards, need a persistent connection. This lesson covers integrating Socket.IO alongside an Express app.
Why WebSockets Need a Shared HTTP Server
Socket.IO (the most popular WebSocket library for Node) attaches itself to the same underlying HTTP server your Express app already uses, rather than running as a separate service. This means you create the raw http server explicitly, pass your Express app into it, and then attach Socket.IO to that same server instance.
Once connected, clients and the server exchange named events over a persistent connection, instead of one request/response cycle per interaction.
Notice server.listen() (the raw HTTP server) is called instead of app.listen(), both Express and Socket.IO share this one server.
Core Socket.IO Events
io.on('connection', (socket) => { ... }); // a client connected
socket.on('eventName', (data) => { ... }); // received a named event
socket.emit('eventName', data); // send to this client only
io.emit('eventName', data); // broadcast to everyone
socket.join('roomName'); // group sockets into a room
io.to('roomName').emit('eventName', data); // send to a specific room
socket.emit() sends to just the connected client that triggered the handler.
io.emit() broadcasts to every currently connected client.
Rooms let you group related sockets (e.g. everyone in a specific chat channel) and target messages to just that group.
Socket.IO automatically falls back to HTTP long-polling if a WebSocket connection can't be established.
Socket.IO Cheatsheet
Common Socket.IO operations for real-time features.
Task
Code
Attach to Express server
new Server(httpServer)
Handle new connections
io.on('connection', (socket) => {...})
Listen for an event
socket.on('eventName', handler)
Send to one client
socket.emit('eventName', data)
Broadcast to all
io.emit('eventName', data)
Join/message a room
socket.join('room1') / io.to('room1').emit(...)
Authenticating Socket Connections
Just like HTTP routes, real-time connections usually need to know who's connecting. Socket.IO middleware can validate a token (often the same JWT used by your REST API) before allowing the connection to proceed.
When running multiple server instances behind a load balancer, a client connected to one instance can't see broadcasts sent from another by default. The Socket.IO Redis adapter synchronizes events across all instances so io.emit() reaches every connected client, regardless of which instance they're attached to.
Calling app.listen() instead of server.listen() after wiring up Socket.IO, breaking the shared server setup.
Broadcasting sensitive data to io.emit() (everyone) when it should be scoped to a specific room or client.
Skipping connection authentication, allowing unauthenticated clients to send arbitrary events.
Running multiple server instances without the Redis adapter, causing inconsistent real-time behavior across instances.
Key Takeaways
Socket.IO attaches to the same raw HTTP server your Express app already uses.
socket.emit() targets one client; io.emit() broadcasts to everyone; rooms scope messages to a group.
Authenticate socket connections just as carefully as HTTP routes.
The Redis adapter is required for consistent real-time behavior across multiple server instances.
Pro Tip
Reuse the exact same JWT verification logic for both your REST routes and your Socket.IO connection middleware, maintaining two separate authentication systems for the same app is a common source of subtle security gaps.
You now know how to add real-time features with Socket.IO. Next, learn how to deploy an Express application in the Deployment lesson.