Socket.IO on an Express Server

To share a port with Express 24,430 you must create the http.Server yourself: app.listen() returns one, but Socket.IO 24,482 needs the instance before it starts listening. The cors 6,195 option is not optional in production — Engine.IO serves the handshake itself and never consults your cors() middleware, so leaving it out lets any page open an authenticated socket.

A Socket.IO server sharing a port with Express, and a client scriptJavaScript
const app = express();                               // your REST routes go on here
const httpServer = createServer(app);                // not app.listen()
const io = new Server(httpServer, { cors: { origin: 'https://app.example.com' } });
io.on('connection', (socket) => {
  console.log('connect  ', socket.id, 'on', socket.conn.transport.name);
  socket.on('watch', (isbn, ack) => {                // the last argument acknowledges
    socket.join(`book:${isbn}`);
    console.log('watch    ', socket.id, '| room size',
      io.sockets.adapter.rooms.get(`book:${isbn}`).size);
    ack({ watching: isbn });
  });
  socket.on('price', ({ isbn, usd }) =>
    io.to(`book:${isbn}`).emit('price', { isbn, usd }));   // socket.to() skips the sender
  socket.on('disconnect', (reason) => console.log('disconnect', socket.id, reason));
});
httpServer.listen(4139);
// ---- io-client.js, a separate process; URL is the origin above, ISBN '978-0441013593' ----
const [a, b] = [io(URL), io(URL)];
for (const [name, sock] of [['a', a], ['b', b]])
  sock.on('price', (p) => console.log(name, 'got price', p.isbn, '$' + p.usd));
a.on('connect', async () => console.log('a ack:', await a.emitWithAck('watch', ISBN)));
b.on('connect', async () => {
  console.log('b ack:', await b.emitWithAck('watch', ISBN));
  b.emit('price', { isbn: ISBN, usd: 12.99 });
  setTimeout(() => { a.close(); b.close(); }, 500);   // triggers the disconnect logs
});
Output
$ node io-server.js                            $ node io-client.js
connect    Hl5K_8oddcdzKgkUAAAA on polling     a ack: { watching: '978-0441013593' }
connect    z7owrMrGiXHKrPdCAAAB on polling     b ack: { watching: '978-0441013593' }
watch      Hl5K_8oddcdzKgkUAAAA | room size 1  a got price 978-0441013593 $12.99
watch      z7owrMrGiXHKrPdCAAAB | room size 2  b got price 978-0441013593 $12.99
disconnect Hl5K_8oddcdzKgkUAAAA client namespace disconnect
disconnect z7owrMrGiXHKrPdCAAAB client namespace disconnect

One price event from b reached both clients because both had joined book:978-0441013593, and emitWithAck turned the server's ack callback into a promise. Both sessions opened on polling and upgrade to websocket a moment later, the Engine.IO behavior Socket.IO explains — also why a browser WebSocket cannot talk to a Socket.IO server. Add connectionStateRecovery: { maxDisconnectionDuration: 60_000 } and a client that drops briefly keeps its socket id, its rooms and the events it missed.