On this page
WebSockets and real-time events in NestJS
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.ioFor raw WebSockets:
npm install @nestjs/websockets @nestjs/platform-ws ws
npm install -D @types/wsThe @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 optionsGateways 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);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);
}
}
Sign in to track your progress