Hasen: Why REST Wasn't Enough and WebSockets Changed Everything
The Problem
I started building Hasen’s multiplayer system assuming I could just use REST endpoints for player communication. Then reality hit: in a card game, when Player A plays a card, Player B needs to know about it immediately and not on the next HTTP request, not after a polling interval, but right now. REST is stateless and request-driven; it can’t push events to a client. I was trying to use a postal service to have a live conversation.
How did I solve it
I replaced the REST approach with WebSockets via Socket.io, organized around the concept of rooms. Each game session became a room: players joined it on connection, and all game events (card plays, turn changes, game over) were broadcast to everyone in that room. The shift in mental model was the real challenge: instead of thinking in terms of “request, response”, I had to think in terms of “events emitted and events received.” Rooms handled isolation between concurrent games, and the server became an event router rather than a data endpoint. Not every feature needs real-time but when it does, reach for sockets without hesitation.
socket.on('player:makeBid', async ({ gameId, bidType, trickNumber, bidId }: { gameId: string; bidType: 'points' | 'set_collection' | 'trick'; trickNumber: 1 | 2 | 3 | 4 | 5; bidId?: string }) => {
try {
const playerData = socketToPlayer.get(socket.id);
if (!playerData) {
socket.emit('error', { message: 'Player not found in session' });
return;
}
const { game, event } = await BidService.makeBid(
gameId,
playerData.playerId,
bidType,
trickNumber,
bidId
);
io.to(gameId).emit('game:event', event);
await BotTurnCoordinator.run(io, gameId);
} catch (error: any) {
console.error('Error in makeBid:', error);
socket.emit('error', { message: error.message });
}
});