Skip to content

WebSockets with Express

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.

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);

io.on('connection', (socket) => {
  console.log('Client connected:', socket.id);

  socket.on('chat message', (msg) => {
    io.emit('chat message', msg); // broadcast to everyone
  });
});

server.listen(3000);

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.

io.use((socket, next) => {
  const token = socket.handshake.auth.token;
  try {
    socket.user = jwt.verify(token, process.env.JWT_SECRET);
    next();
  } catch {
    next(new Error('Authentication failed'));
  }
});

Scaling WebSockets Across Multiple Instances

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.

const { createAdapter } = require('@socket.io/redis-adapter');
const { createClient } = require('redis');

const pubClient = createClient({ url: process.env.REDIS_URL });
const subClient = pubClient.duplicate();

io.adapter(createAdapter(pubClient, subClient));

Common Mistakes

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