On this page

WebSockets and real-time events in NestJS

14 min read TextCh. 4 — Security and Communication

WebSockets in NestJS

While REST APIs follow a request-response model, WebSockets maintain a persistent, bidirectional connection between client and server. This makes them ideal for real-time features like notifications, chat, live dashboards, and collaborative editing.

NestJS provides first-class WebSocket support through gateways — classes that handle WebSocket events using the same decorator-based approach as controllers.

Installation

NestJS supports two WebSocket adapters: socket.io (default) and ws (raw WebSockets).

For socket.io:

npm install @nestjs/websockets @nestjs/platform-socket.io socket.io

For raw WebSockets:

npm install @nestjs/websockets @nestjs/platform-ws ws
npm install -D @types/ws

The @WebSocketGateway decorator

A gateway is a class decorated with @WebSocketGateway():

@WebSocketGateway()                          // Runs on the same port as HTTP
@WebSocketGateway(3001)                      // Runs on a separate port
@WebSocketGateway({ namespace: '/chat' })    // Namespaced
@WebSocketGateway({ cors: { origin: '*' } }) // With CORS options

Gateways are providers — they are registered in a module's providers array and can inject other services normally.

Lifecycle hooks

Implement OnGatewayInit, OnGatewayConnection, and OnGatewayDisconnect to react to the server lifecycle:

import {
  OnGatewayInit,
  OnGatewayConnection,
  OnGatewayDisconnect,
} from '@nestjs/websockets';
import { Server, Socket } from 'socket.io';

@WebSocketGateway()
export class ChatGateway
  implements OnGatewayInit, OnGatewayConnection, OnGatewayDisconnect {

  @WebSocketServer()
  server: Server;

  afterInit(server: Server): void {
    console.log('WebSocket server initialized', server);
  }

  handleConnection(client: Socket, ...args: unknown[]): void {
    console.log(`Client connected: ${client.id}`, args);
  }

  handleDisconnect(client: Socket): void {
    console.log(`Client disconnected: ${client.id}`);
  }
}

Handling events with @SubscribeMessage

Define event handlers with the @SubscribeMessage() decorator:

@SubscribeMessage('message')
handleMessage(
  @ConnectedSocket() client: Socket,
  @MessageBody() payload: { roomId: string; text: string },
): void {
  // Broadcast to everyone in the room (including sender)
  this.server.to(payload.roomId).emit('message', {
    id: crypto.randomUUID(),
    text: payload.text,
    senderId: client.id,
    timestamp: new Date().toISOString(),
  });
}

The return value of a @SubscribeMessage handler is automatically sent back to the sender as an acknowledgment.

Chat room example

@WebSocketGateway({ namespace: 'chat' })
export class ChatGateway {
  @WebSocketServer()
  server: Server;

  @SubscribeMessage('join-room')
  handleJoinRoom(
    @ConnectedSocket() client: Socket,
    @MessageBody() roomId: string,
  ): void {
    client.join(roomId);
    client.to(roomId).emit('user-joined', { userId: client.id });
    client.emit('joined', { roomId });
  }

  @SubscribeMessage('leave-room')
  handleLeaveRoom(
    @ConnectedSocket() client: Socket,
    @MessageBody() roomId: string,
  ): void {
    client.leave(roomId);
    client.to(roomId).emit('user-left', { userId: client.id });
  }

  @SubscribeMessage('send-message')
  handleSendMessage(
    @ConnectedSocket() client: Socket,
    @MessageBody() payload: { roomId: string; message: string },
  ): void {
    this.server.to(payload.roomId).emit('new-message', {
      userId: client.id,
      message: payload.message,
      timestamp: new Date().toISOString(),
    });
  }
}

WsException for WebSocket errors

Use WsException instead of HttpException in WebSocket handlers:

import { WsException } from '@nestjs/websockets';

@SubscribeMessage('send-message')
handleMessage(@MessageBody() payload: unknown): void {
  if (!payload || typeof payload !== 'object') {
    throw new WsException('Invalid payload format');
  }
  // ...
}

Applying guards to WebSocket handlers

Guards work in WebSocket context but need to use the WS execution context:

@Injectable()
export class WsAuthGuard implements CanActivate {
  constructor(private readonly jwtService: JwtService) {}

  canActivate(context: ExecutionContext): boolean {
    const client = context.switchToWs().getClient<Socket>();
    const token = client.handshake.auth['token'] as string | undefined;

    if (!token) throw new WsException('No token provided');

    try {
      const payload = this.jwtService.verify<JwtPayload>(token);
      client.data['user'] = payload;
      return true;
    } catch {
      throw new WsException('Invalid token');
    }
  }
}

interface JwtPayload {
  sub: number;
  email: string;
  role: string;
}

Clients send the token during connection:

const socket = io('http://localhost:3000/notifications', {
  auth: { token: 'Bearer eyJhbGci...' },
});

Event emitter for decoupled notifications

NestJS's EventEmitter2 module enables loosely coupled communication between services:

npm install @nestjs/event-emitter eventemitter2
// In AppModule
EventEmitterModule.forRoot({ wildcard: true, delimiter: '.' })

// Emitting an event from a service
@Injectable()
export class BooksService {
  constructor(private readonly eventEmitter: EventEmitter2) {}

  async create(dto: CreateBookDto): Promise<Book> {
    const book = await this.save(dto);
    this.eventEmitter.emit('book.created', { book });
    return book;
  }
}

// Listening in the gateway or another service
@OnEvent('book.created')
handleBookCreated(payload: { book: Book }): void {
  this.server.emit('book:new', payload.book);
}

Scaling WebSockets

For multi-instance deployments, use the Redis adapter so WebSocket events are shared across all instances:

npm install @nestjs/platform-socket.io @socket.io/redis-adapter redis
// main.ts
const redisClient = createClient({ url: process.env['REDIS_URL'] });
await redisClient.connect();
const subClient = redisClient.duplicate();
await subClient.connect();

const adapter = createAdapter(redisClient, subClient);
app.useWebSocketAdapter(new IoAdapter(app));
(app as INestApplication).getHttpAdapter().getInstance().adapter(adapter);
Room-based broadcasting
Socket.IO rooms let you group clients and broadcast to all of them at once. Use client.join('room-name') when a client subscribes to a topic, then this.server.to('room-name').emit('event', data) to broadcast to all subscribers. This is perfect for notifications per user, per organization, or per document.
WebSocket guards use WsException
Guards applied to WebSocket message handlers should throw WsException instead of HttpException. Throwing an HttpException in a WebSocket context sends the error as a WebSocket message, which can confuse the client. Import WsException from @nestjs/websockets.
import {
  WebSocketGateway,
  WebSocketServer,
  SubscribeMessage,
  MessageBody,
  ConnectedSocket,
  OnGatewayConnection,
  OnGatewayDisconnect,
} from '@nestjs/websockets';
import { Server, Socket } from 'socket.io';
import { UseGuards } from '@nestjs/common';
import { WsAuthGuard } from '../auth/ws-auth.guard';

@WebSocketGateway({
  cors: { origin: 'http://localhost:4200', credentials: true },
  namespace: 'notifications',
})
export class NotificationsGateway
  implements OnGatewayConnection, OnGatewayDisconnect {

  @WebSocketServer()
  server: Server;

  private connectedUsers = new Map<string, string>(); // socketId → userId

  handleConnection(client: Socket): void {
    console.log(`Client connected: ${client.id}`);
  }

  handleDisconnect(client: Socket): void {
    this.connectedUsers.delete(client.id);
    console.log(`Client disconnected: ${client.id}`);
  }

  @UseGuards(WsAuthGuard)
  @SubscribeMessage('join')
  handleJoin(
    @MessageBody() data: { userId: string },
    @ConnectedSocket() client: Socket,
  ): void {
    this.connectedUsers.set(client.id, data.userId);
    client.join(`user:${data.userId}`);
    client.emit('joined', { message: 'Connected to notifications' });
  }

  // Called by services to push to specific users
  sendToUser(userId: string, event: string, payload: unknown): void {
    this.server.to(`user:${userId}`).emit(event, payload);
  }
}