En esta página

WebSockets y comunicación en tiempo real

14 min lectura TextoCap. 4 — Seguridad y comunicación

WebSockets en NestJS

Los WebSockets permiten comunicación bidireccional en tiempo real entre el servidor y los clientes. A diferencia de HTTP que es request-response, WebSockets mantienen una conexión persistente que permite al servidor enviar datos al cliente sin que este los solicite.

NestJS integra Socket.IO de forma nativa a través de @nestjs/websockets, ofreciendo la misma filosofía de decoradores y DI que ya conoces del lado HTTP.

Instalación

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

El decorador @WebSocketGateway

Un gateway es el equivalente WebSocket de un controlador HTTP. Se configura con el decorador @WebSocketGateway:

@WebSocketGateway()                        // puerto 3000 por defecto
@WebSocketGateway(8080)                    // puerto específico
@WebSocketGateway({ namespace: '/chat' })  // namespace específico
@WebSocketGateway({
  cors: { origin: '*' },                   // CORS para WebSockets
  transports: ['websocket', 'polling'],    // transportes permitidos
  pingInterval: 10000,                     // ping cada 10 segundos
  pingTimeout: 5000,                       // timeout de ping
})

Ciclo de vida del Gateway

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

@WebSocketGateway()
export class NotificacionesGateway
  implements OnGatewayInit, OnGatewayConnection, OnGatewayDisconnect
{
  @WebSocketServer()
  server: Server;

  // Llamado después de que el servidor WebSocket se inicializa
  afterInit(_server: Server): void {
    console.log('Gateway inicializado');
  }

  // Llamado cuando un cliente se conecta
  handleConnection(client: Socket): void {
    console.log(`Conectado: ${client.id}`);
  }

  // Llamado cuando un cliente se desconecta
  handleDisconnect(client: Socket): void {
    console.log(`Desconectado: ${client.id} — Razón: ${client.disconnected}`);
  }
}

Manejando mensajes con @SubscribeMessage

import { SubscribeMessage, MessageBody, ConnectedSocket } from '@nestjs/websockets';

@WebSocketGateway({ namespace: '/notificaciones' })
export class NotificacionesGateway {
  @WebSocketServer()
  server: Server;

  // Respuesta simple (retorno del método)
  @SubscribeMessage('saludo')
  handleSaludo(@MessageBody() nombre: string): string {
    return `¡Hola, ${nombre}!`;
  }

  // Emitir a un evento diferente en lugar de hacer ack
  @SubscribeMessage('marcarLeido')
  handleMarcarLeido(
    @MessageBody() data: { notificacionId: string },
    @ConnectedSocket() client: Socket,
  ): void {
    // Emitir a todos EXCEPTO al cliente actual
    client.broadcast.emit('notificacionLeida', data.notificacionId);

    // Emitir solo al cliente actual
    client.emit('confirmacionLeido', { ok: true });

    // Emitir a todos (incluyendo al cliente)
    this.server.emit('contadorActualizado', { pendientes: 0 });
  }
}

Rooms — Grupos de clientes

Las rooms permiten agrupar clientes y emitir mensajes solo a ese grupo:

// Unirse a una sala
@SubscribeMessage('joinRoom')
async handleJoinRoom(
  @MessageBody() roomId: string,
  @ConnectedSocket() client: Socket,
): Promise<void> {
  await client.join(roomId);

  // Notificar a todos en la sala que alguien se unió
  this.server.to(roomId).emit('userJoined', { id: client.id });
}

// Abandonar una sala
@SubscribeMessage('leaveRoom')
async handleLeaveRoom(
  @MessageBody() roomId: string,
  @ConnectedSocket() client: Socket,
): Promise<void> {
  await client.leave(roomId);
  this.server.to(roomId).emit('userLeft', { id: client.id });
}

// Emitir a una sala específica desde cualquier punto
emitirASala(roomId: string, evento: string, datos: unknown): void {
  this.server.to(roomId).emit(evento, datos);
}

Emitir eventos desde servicios HTTP

Un patrón muy útil: emitir eventos WebSocket cuando ocurren acciones HTTP. Por ejemplo, notificar a los usuarios conectados cuando se crea un nuevo pedido:

// pedidos/pedidos.service.ts
@Injectable()
export class PedidosService {
  constructor(
    @InjectRepository(Pedido) private readonly repo: Repository<Pedido>,
    private readonly notificacionesGateway: NotificacionesGateway,
  ) {}

  async crearPedido(dto: CrearPedidoDto, usuarioId: string): Promise<Pedido> {
    const pedido = await this.repo.save(this.repo.create({ ...dto, usuarioId }));

    // Notificar en tiempo real al usuario que creó el pedido
    this.notificacionesGateway.emitirAUsuario(usuarioId, 'pedidoCreado', {
      pedidoId: pedido.id,
      total: pedido.total,
      mensaje: 'Tu pedido ha sido creado con éxito',
    });

    return pedido;
  }
}

Filtros de excepción para WebSockets

import { Catch, ArgumentsHost, BadRequestException } from '@nestjs/common';
import { BaseWsExceptionFilter, WsException } from '@nestjs/websockets';
import { Socket } from 'socket.io';

@Catch(WsException, BadRequestException)
export class WsExceptionFilter extends BaseWsExceptionFilter {
  catch(exception: WsException | BadRequestException, host: ArgumentsHost): void {
    const client = host.switchToWs().getClient<Socket>();
    const mensaje =
      exception instanceof WsException
        ? exception.getError()
        : exception.message;

    client.emit('error', {
      tipo: 'error',
      mensaje,
      timestamp: new Date().toISOString(),
    });
  }
}

Cliente Socket.IO en el frontend

Para conectarte desde Angular:

// En Angular — instalación: npm install socket.io-client
import { io } from 'socket.io-client';

const socket = io('http://localhost:3000/chat', {
  auth: {
    token: 'tu_jwt_token_aqui',
  },
  transports: ['websocket'],
});

socket.on('connect', () => console.log('Conectado al chat'));
socket.on('nuevoMensaje', (mensaje) => console.log('Nuevo mensaje:', mensaje));

// Enviar un mensaje
socket.emit('enviarMensaje', { salaId: 'sala-1', contenido: '¡Hola a todos!' });

// Manejar reconexión automática
socket.on('disconnect', (reason) => {
  if (reason === 'io server disconnect') {
    socket.connect(); // reconectar manualmente si el servidor nos desconectó
  }
});

Los WebSockets añaden una dimensión completamente nueva a tus APIs: la comunicación en tiempo real. En la próxima lección aprenderemos a gestionar la configuración de la aplicación con variables de entorno y @nestjs/config.

Namespaces vs Rooms en Socket.IO
Los namespaces (`/chat`, `/notificaciones`) dividen el servidor WebSocket en conexiones lógicamente separadas —cada namespace tiene su propio middleware y control de eventos. Las rooms son agrupaciones dentro de un namespace que permiten emitir a subconjuntos de clientes conectados. Usa namespaces para separar dominios funcionales y rooms para separar grupos de usuarios dentro de un dominio.
Emitir desde servicios usando el Gateway como dependencia
Puedes inyectar el gateway en otros servicios para emitir eventos WebSocket desde la lógica de negocio. Por ejemplo, cuando se crea un nuevo pedido en `PedidosService`, puedes inyectar `ChatGateway` o `NotificacionesGateway` para notificar al usuario en tiempo real. Exporta el gateway desde su módulo para poder inyectarlo en otros módulos.