WebSockets & Real-Time Apps

Rooms and Namespaces

19 min Lesson 9 of 35

Rooms and Namespaces

Rooms and namespaces are powerful features in Socket.io that allow you to organize connections and broadcast messages to specific groups of clients. Understanding when to use rooms versus namespaces is crucial for building scalable real-time applications.

What Are Rooms?

Rooms are arbitrary channels that sockets can join and leave. They allow you to broadcast events to a subset of connected clients:

// Think of rooms like chat channels // A socket can be in multiple rooms simultaneously // Examples: // - room:general (chat channel) // - room:support (chat channel) // - game:123 (specific game instance) // - user:john (private room for a user)
Note: Rooms are a server-only concept. Clients cannot directly join rooms - they must request the server to join them to a room. This gives the server full control over room membership.

Joining and Leaving Rooms

Sockets can join and leave rooms using simple methods:

// SERVER io.on('connection', (socket) => { console.log('User connected:', socket.id); // Join a room socket.join('general'); console.log(`${socket.id} joined room: general`); // Join multiple rooms socket.join(['general', 'announcements', 'vip']); // Leave a room socket.leave('general'); // Check if socket is in a room const isInRoom = socket.rooms.has('general'); console.log('In general room?', isInRoom); // Get all rooms the socket is in console.log('Socket rooms:', socket.rooms); // Set { 'socketId', 'general', 'announcements', 'vip' } });

Client-Requested Room Joining

Typically, clients request to join specific rooms:

// CLIENT socket.emit('joinRoom', 'general'); socket.emit('leaveRoom', 'support'); // SERVER io.on('connection', (socket) => { // Handle join room request socket.on('joinRoom', (roomName) => { // Validate room name if (!roomName || typeof roomName !== 'string') { socket.emit('error', 'Invalid room name'); return; } // Join the room socket.join(roomName); // Notify user socket.emit('roomJoined', roomName); // Notify others in the room socket.to(roomName).emit('userJoined', { userId: socket.id, room: roomName }); console.log(`${socket.id} joined ${roomName}`); }); // Handle leave room request socket.on('leaveRoom', (roomName) => { socket.leave(roomName); socket.emit('roomLeft', roomName); socket.to(roomName).emit('userLeft', { userId: socket.id, room: roomName }); }); });

Broadcasting to Rooms

Send messages to all sockets in a specific room:

// Broadcast to everyone in 'general' room io.to('general').emit('message', 'Hello general room!'); // Broadcast to multiple rooms io.to('general').to('vip').emit('announcement', 'Special message'); // Broadcast to room except sender socket.to('general').emit('message', 'Someone else sent this'); // Broadcast to room including sender io.in('general').emit('message', 'Everyone in general receives this'); // Note: to() and in() are identical - use whichever you prefer

Room Management

Get information about rooms and their members:

// Get all sockets in a room (Socket.io 4.0+) const socketsInRoom = await io.in('general').fetchSockets(); console.log(`${socketsInRoom.length} users in general`); socketsInRoom.forEach(socket => { console.log('Socket ID:', socket.id); console.log('Rooms:', socket.rooms); }); // Get all room names const rooms = io.sockets.adapter.rooms; console.log('All rooms:', Array.from(rooms.keys())); // Get number of sockets in a room const roomSize = io.sockets.adapter.rooms.get('general')?.size || 0; console.log('Users in general:', roomSize); // Check if room exists const roomExists = io.sockets.adapter.rooms.has('general'); console.log('General room exists?', roomExists);

Practical Room Example: Chat Rooms

// SERVER const chatRooms = new Map(); // Track room metadata io.on('connection', (socket) => { console.log('User connected:', socket.id); // Join room socket.on('joinRoom', async (roomName, username) => { // Leave current rooms (except socket's own room) Array.from(socket.rooms).forEach(room => { if (room !== socket.id) { socket.leave(room); } }); // Join new room socket.join(roomName); // Track room metadata if (!chatRooms.has(roomName)) { chatRooms.set(roomName, { users: [] }); } chatRooms.get(roomName).users.push({ id: socket.id, username }); // Send room info to user const socketsInRoom = await io.in(roomName).fetchSockets(); socket.emit('roomInfo', { room: roomName, userCount: socketsInRoom.length, users: chatRooms.get(roomName).users }); // Notify room socket.to(roomName).emit('userJoined', { userId: socket.id, username: username }); console.log(`${username} joined ${roomName}`); }); // Send message to room socket.on('roomMessage', (roomName, message) => { io.to(roomName).emit('roomMessage', { userId: socket.id, message: message, timestamp: new Date() }); }); // Handle disconnect socket.on('disconnect', () => { // Remove from all room metadata chatRooms.forEach((roomData, roomName) => { const userIndex = roomData.users.findIndex(u => u.id === socket.id); if (userIndex !== -1) { const username = roomData.users[userIndex].username; roomData.users.splice(userIndex, 1); // Notify room io.to(roomName).emit('userLeft', { userId: socket.id, username: username }); } }); }); });

What Are Namespaces?

Namespaces allow you to split your application logic across multiple communication channels on the same server:

// Think of namespaces like separate Socket.io instances // Each namespace has its own: // - Event handlers // - Rooms // - Middleware // - Connected clients // Examples: // /chat - chat functionality // /admin - admin panel // /notifications - notification system // /game - game logic
Tip: Use namespaces for different application features (chat vs admin), and use rooms within a namespace to organize users (chat:general vs chat:support).

Creating Namespaces

Define namespaces on the server:

// SERVER const express = require('express'); const { createServer } = require('http'); const { Server } = require('socket.io'); const app = express(); const httpServer = createServer(app); const io = new Server(httpServer); // Default namespace '/' (already exists) io.on('connection', (socket) => { console.log('Connected to default namespace'); }); // Create /chat namespace const chatNamespace = io.of('/chat'); chatNamespace.on('connection', (socket) => { console.log('Connected to /chat namespace:', socket.id); socket.on('message', (msg) => { chatNamespace.emit('message', msg); }); }); // Create /admin namespace const adminNamespace = io.of('/admin'); adminNamespace.on('connection', (socket) => { console.log('Admin connected:', socket.id); socket.on('adminAction', (action) => { console.log('Admin action:', action); }); }); // Create /notifications namespace const notifNamespace = io.of('/notifications'); notifNamespace.on('connection', (socket) => { socket.emit('welcome', 'Connected to notifications'); }); httpServer.listen(3000);

Connecting to Namespaces (Client)

// CLIENT // Connect to default namespace const socket = io('http://localhost:3000'); // Connect to /chat namespace const chatSocket = io('http://localhost:3000/chat'); // Connect to /admin namespace const adminSocket = io('http://localhost:3000/admin'); // Connect to /notifications namespace const notifSocket = io('http://localhost:3000/notifications'); // Use different namespaces chatSocket.on('connect', () => { console.log('Connected to chat'); chatSocket.emit('message', 'Hello from chat!'); }); adminSocket.on('connect', () => { console.log('Connected to admin'); adminSocket.emit('adminAction', 'view-users'); }); notifSocket.on('welcome', (msg) => { console.log(msg); });

Namespace-Specific Events and Rooms

Each namespace has its own rooms and events:

// SERVER const chatNamespace = io.of('/chat'); chatNamespace.on('connection', (socket) => { // Join rooms within /chat namespace socket.join('general'); socket.join('tech'); // Broadcast within namespace chatNamespace.emit('userCount', chatNamespace.sockets.size); // Broadcast to room within namespace socket.to('general').emit('userJoined', socket.id); // Handle events within namespace socket.on('chatMessage', (room, message) => { chatNamespace.to(room).emit('chatMessage', { room: room, message: message, sender: socket.id }); }); }); // Rooms in different namespaces are independent const adminNamespace = io.of('/admin'); adminNamespace.on('connection', (socket) => { // This 'general' room is separate from chat's 'general' room socket.join('general'); });

Dynamic Namespaces

Create namespaces dynamically based on patterns:

// SERVER // Create namespace for each organization io.of(/^\/org-\w+$/).on('connection', (socket) => { const namespace = socket.nsp.name; // e.g., '/org-acme' console.log(`Connected to ${namespace}`); socket.on('message', (msg) => { // Broadcast to this specific organization socket.nsp.emit('message', msg); }); }); // CLIENT const acmeSocket = io('http://localhost:3000/org-acme'); const xyzSocket = io('http://localhost:3000/org-xyz');

Room vs Namespace: When to Use Which?

// Use NAMESPACES for: // ✓ Separating different application features // ✓ Different authentication/authorization logic // ✓ Different middleware per feature // ✓ Completely separate event handlers // Example: /chat, /admin, /game // Use ROOMS for: // ✓ Grouping users within the same feature // ✓ Temporary groups (chat rooms, game lobbies) // ✓ Broadcasting to subsets of users // ✓ Same event handlers, different recipients // Example: chat:general, chat:support, game:lobby-1

Complete Example: Chat with Rooms and Namespaces

// SERVER const io = require('socket.io')(3000); // /chat namespace with rooms const chat = io.of('/chat'); chat.on('connection', (socket) => { console.log('User connected to chat'); // Join default room socket.join('general'); chat.to('general').emit('notification', 'New user joined general'); // Switch rooms socket.on('switchRoom', (newRoom) => { const currentRooms = Array.from(socket.rooms).filter(r => r !== socket.id); currentRooms.forEach(room => { socket.leave(room); chat.to(room).emit('notification', `User left ${room}`); }); socket.join(newRoom); chat.to(newRoom).emit('notification', `User joined ${newRoom}`); socket.emit('roomChanged', newRoom); }); // Send message to current rooms socket.on('message', async (message) => { const rooms = Array.from(socket.rooms).filter(r => r !== socket.id); rooms.forEach(room => { chat.to(room).emit('message', { room: room, sender: socket.id, message: message }); }); }); socket.on('disconnect', () => { console.log('User disconnected from chat'); }); }); // /admin namespace (no rooms needed) const admin = io.of('/admin'); admin.on('connection', (socket) => { console.log('Admin connected'); socket.on('getStats', async () => { const chatSockets = await chat.fetchSockets(); socket.emit('stats', { chatUsers: chatSockets.length, adminUsers: admin.sockets.size }); }); });
Exercise: Build a multi-room chat system:
  • Create a /chat namespace with three rooms: general, tech, and random
  • Allow users to join and switch between rooms
  • Broadcast join/leave notifications to room members only
  • Implement a command to list all users in the current room
  • Add an /admin namespace that can see statistics for all rooms
  • Ensure messages are only sent to the correct room
  • Clean up room metadata when users disconnect