In the BhuMitra app, surveyors out in the field can invite clients to a "Live Room". When the surveyor walks around the perimeter of a farm, their GPS location is beamed to the server, and instantly broadcast to everyone else in the room. This looks like magic to the user, but under the hood, it's a careful orchestration of NestJS WebSockets, Socket.io rooms, and a Redis adapter.
Here is how to build production-grade, horizontally scalable WebSocket rooms in NestJS.
1. Why Socket.io and Redis?
Standard WebSockets (ws) are great, but Socket.io provides built-in fallback to HTTP long-polling, automatic reconnection, and most importantly, Rooms. A Room is an arbitrary channel that sockets can join and leave.
However, if you scale your backend to multiple Node.js instances (e.g., via PM2 or Kubernetes), User A might connect to Server 1, and User B might connect to Server 2. If Server 1 broadcasts a message to the room, User B won't receive it! This is where the Redis Adapter comes in. It uses Redis Pub/Sub to forward events between all your server instances.
2. Setup and Installation
npm i @nestjs/websockets @nestjs/platform-socket.io socket.io npm i @socket.io/redis-adapter redis
3. Creating the Redis Adapter
We need to tell NestJS to use the Redis adapter instead of the default in-memory adapter.
// src/redis-io.adapter.ts
import { IoAdapter } from '@nestjs/platform-socket.io';
import { ServerOptions } from 'socket.io';
import { createAdapter } from '@socket.io/redis-adapter';
import { createClient } from 'redis';
export class RedisIoAdapter extends IoAdapter {
private adapterConstructor: ReturnType<typeof createAdapter>;
async connectToRedis(): Promise<void> {
const pubClient = createClient({ url: `redis://localhost:6379` });
const subClient = pubClient.duplicate();
await Promise.all([pubClient.connect(), subClient.connect()]);
this.adapterConstructor = createAdapter(pubClient, subClient);
}
createIOServer(port: number, options?: ServerOptions): any {
const server = super.createIOServer(port, options);
server.adapter(this.adapterConstructor);
return server;
}
}
In your main.ts, hook it up:
async function bootstrap() {
const app = await NestFactory.create(AppModule);
const redisIoAdapter = new RedisIoAdapter(app);
await redisIoAdapter.connectToRedis();
app.useWebSocketAdapter(redisIoAdapter);
await app.listen(3000);
}
4. Building the Gateway
Gateways in NestJS are classes annotated with @WebSocketGateway(). They handle incoming events and broadcast to rooms.
// src/live-room/live-room.gateway.ts
import {
WebSocketGateway,
SubscribeMessage,
MessageBody,
ConnectedSocket,
WebSocketServer,
OnGatewayDisconnect,
} from '@nestjs/websockets';
import { Server, Socket } from 'socket.io';
@WebSocketGateway({ namespace: '/survey-rooms', cors: true })
export class LiveRoomGateway implements OnGatewayDisconnect {
@WebSocketServer()
server: Server;
@SubscribeMessage('joinRoom')
handleJoinRoom(
@MessageBody() data: { roomId: string; userId: string },
@ConnectedSocket() client: Socket,
) {
client.join(data.roomId);
// Notify others in the room
client.to(data.roomId).emit('userJoined', {
userId: data.userId,
message: 'A new user joined the survey.',
});
return { status: 'Joined', roomId: data.roomId };
}
@SubscribeMessage('updateLocation')
handleLocationUpdate(
@MessageBody() data: { roomId: string; lat: number; lng: number },
@ConnectedSocket() client: Socket,
) {
// Broadcast location to everyone in the room EXCEPT the sender
client.to(data.roomId).emit('locationChanged', {
lat: data.lat,
lng: data.lng,
});
}
handleDisconnect(client: Socket) {
// Socket.io automatically removes the client from all rooms,
// but you can log the disconnection here.
console.log(`Client disconnected: ${client.id}`);
}
}
5. Security Considerations
WebSockets bypass standard HTTP middleware. You must protect your Gateway using standard NestJS Guards, extracting the JWT token from the WebSocket handshake (client.handshake.headers.authorization) and verifying it before allowing the user to join a room.
Conclusion
WebSockets are powerful, but stateful connections in a stateless REST world require careful planning. By offloading room state to Redis, you guarantee that your WebSocket server can scale horizontally without dropping real-time events.
— Ankit Kumar