Rooms let you group clients so messages only go to people in that group â like a group chat, a game lobby, or a collaborative document. The ws library has no built-in rooms, so you build them yourself with a Map of Sets.
// rooms = Map< roomName: string, clients: Set<WebSocket> >
const rooms = new Map();
// server.js
const { WebSocket } = require('ws');
const rooms = new Map();
// ââ Join a room ââââââââââââââââââââââââââââââââââââââââ
function joinRoom(ws, roomName) {
if (!rooms.has(roomName)) {
rooms.set(roomName, new Set());
}
rooms.get(roomName).add(ws);
// Track which rooms this socket is in
ws.currentRooms = ws.currentRooms || new Set();
ws.currentRooms.add(roomName);
console.log(`${ws.clientId} joined '${roomName}' (${rooms.get(roomName).size} members)`);
}
// ââ Leave a room âââââââââââââââââââââââââââââââââââââââ
function leaveRoom(ws, roomName) {
rooms.get(roomName)?.delete(ws);
ws.currentRooms?.delete(roomName);
// Clean up empty rooms
if (rooms.get(roomName)?.size === 0) {
rooms.delete(roomName);
console.log(`Room '${roomName}' deleted (empty)`);
}
}
// ââ Broadcast to a room (excluding sender) âââââââââââââ
function toRoom(roomName, data, excludeWs = null) {
const room = rooms.get(roomName);
if (!room) return;
room.forEach((client) => {
if (client !== excludeWs && client.readyState === WebSocket.OPEN) {
client.send(JSON.stringify(data));
}
});
}
// ââ Get room info ââââââââââââââââââââââââââââââââââââââ
function getRoomInfo(roomName) {
return {
name: roomName,
members: rooms.get(roomName)?.size ?? 0,
};
}
wss.on('connection', (ws) => {
ws.on('message', (raw) => {
const msg = JSON.parse(raw);
switch (msg.type) {
case 'join':
joinRoom(ws, msg.room);
// Notify everyone in room that someone joined
toRoom(msg.room, {
type: 'system',
text: `${ws.username} joined the room`,
room: msg.room,
}, ws);
// Confirm to the joining client
ws.send(JSON.stringify({
type: 'joined',
room: msg.room,
info: getRoomInfo(msg.room),
}));
break;
case 'leave':
leaveRoom(ws, msg.room);
toRoom(msg.room, {
type: 'system',
text: `${ws.username} left the room`,
});
break;
case 'message':
toRoom(msg.room, {
type: 'message',
text: msg.text,
from: ws.username,
room: msg.room,
timestamp: Date.now(),
}, ws); // exclude sender
break;
}
});
// On disconnect â leave ALL rooms this socket was in
ws.on('close', () => {
ws.currentRooms?.forEach((roomName) => {
leaveRoom(ws, roomName);
});
});
});
// client.js
const ws = new WebSocket('ws://localhost:8080');
ws.onopen = () => {
// Join a room
ws.send(JSON.stringify({
type: 'join',
room: 'gaming',
}));
};
ws.onmessage = (event) => {
const data = JSON.parse(event.data);
if (data.type === 'message') {
console.log(`[${data.room}] ${data.from}: ${data.text}`);
}
if (data.type === 'system') {
console.log(`âšī¸ ${data.text}`);
}
if (data.type === 'joined') {
console.log(`Joined '${data.room}' â ${data.info.members} members`);
}
};
// Send a message to a room
function sendToRoom(room, text) {
ws.send(JSON.stringify({ type: 'message', room, text }));
}
// Leave a room
function leaveRoom(room) {
ws.send(JSON.stringify({ type: 'leave', room }));
}
A single client can be in multiple rooms simultaneously. The ws.currentRooms Set tracks this:
// Client joins multiple rooms
ws.send(JSON.stringify({ type: 'join', room: 'gaming' }));
ws.send(JSON.stringify({ type: 'join', room: 'announcements' }));
ws.send(JSON.stringify({ type: 'join', room: 'team-alpha' }));
// On disconnect the server automatically cleans up all 3 rooms
Want rooms with zero boilerplate? Socket.io gives you
socket.join('room')andio.to('room').emit()built-in. See Chapter 9.