WebSocket

Tempo real com WebSocket

Gateway Socket.IO compartilhado, autenticação JWT no handshake e handlers por tipo de mensagem.

Visão geral

A infraestrutura em src/common/websocket expõe um único gateway Socket.IO. Features (chat, notificações, etc.) não criam outro gateway: registram MessageHandlers que respondem a payload.type. O módulo de referência é websocket-example.

Ideias-chave

  • • Um gateway compartilhado em path /ws
  • • Evento Socket.IO único: message
  • • Roteamento por payload.type → MessageHandler
  • • JWT obrigatório no handshake de conexão

Arquitetura e fluxo

O AppModule importa WebsocketModule.forRoot(), que sobe WebsocketGateway e WebsocketService. Cada feature module importa WebsocketModule e registra seus handlers.

Caminho de uma mensagem

Cliente conecta em /ws com JWT

Cliente emite message com { type, ... }

WebsocketGateway @SubscribeMessage('message')

WebsocketService.processMessage encontra o handler

Handler valida DTO e chama o service da feature

Gateway responde messageResponse { received: true }

TypeScriptAppModule
// app.module.ts
imports: [
  WebsocketModule.forRoot(),
  WebsocketExampleModule, // módulos de feature
  // MyFeatureModule,
]

Autenticação JWT

No afterInit do AbstractWebsocketGateway, um middleware Socket.IO exige JWT. JwtAuthGuard e RolesGuard também atuam no gateway WS. Handlers podem declarar roles para autorização por mensagem.

  • • Token em socket.handshake.auth.token
  • • Ou header Authorization: Bearer <token>
  • • Verificação com jwt.secret (ConfigService)
  • • decoded.id / email / roles → socket.data.user
  • • handler.roles opcional — ex.: websocket-example exige Role.SUPER
Clienthandshake
const socket = io('http://localhost:3000', {
  path: '/ws',
  auth: { token: accessToken },
  // ou: extraHeaders: { Authorization: `Bearer ${accessToken}` }
});

Contrato de mensagem

Todo tráfego de feature passa pelo evento message. O campo type seleciona o handler (first-match).

Interfaces

TypeScriptmessage-handler.interface.ts
export interface MessagePayload {
  type: string;
  [key: string]: any;
}

export interface MessageHandler {
  canHandle(type: string): boolean;
  handle(client: Socket, payload: MessagePayload): void;
}

Eventos

  • • message (client → server) — payload com type
  • • messageResponse (server → client) — ack do gateway
  • • error — validação falhou (padrão do example)
  • • Eventos de feature — definidos por você (ex.: websocket_example_ack)

API do WebsocketService

O service concreto estende AbstractWebsocketService e é o ponto de registro/broadcast.

  • registerMessageHandler(handler) — registra um handler
  • addClient / removeClient / getClient / getAllClients
  • sendToClient(clientId, event, data)
  • broadcast(event, data, except?)
  • processMessage(client, payload) — first handler com canHandle(type)

Criar um módulo WebSocket

Siga estes passos para uma feature completa. Não é necessário criar um novo @WebSocketGateway.

Estrutura de pastas

Filesystemsrc/modules/my-feature
src/modules/my-feature/
├── my-feature.module.ts
├── my-feature.service.ts
├── dtos/
│   └── my-feature.dto.ts
└── handlers/
    └── my-feature.handler.ts

1. DTO

Inclua type estável e valide com class-validator.

TypeScriptDTO
export class MyFeatureDto {
  @IsString()
  @IsNotEmpty()
  type: string = 'my_feature';  // type estável — chave do roteamento

  @IsString()
  @IsNotEmpty()
  message!: string;
}

2. Handler

Implemente MessageHandler: canHandle + validate + service.

TypeScriptHandler
@Injectable()
export class MyFeatureHandler implements MessageHandler {
  constructor(private readonly myFeatureService: MyFeatureService) {}

  canHandle(type: string): boolean {
    return type === 'my_feature';
  }

  async handle(client: Socket, payload: MessagePayload) {
    const dto = Object.assign(new MyFeatureDto(), payload);
    const errors = await validate(dto);
    if (errors.length > 0) {
      client.emit('error', { message: 'Validation failed', errors });
      return;
    }
    this.myFeatureService.processMessage(client, dto);
  }
}

3. Service

Lógica de negócio; use client.emit ou WebsocketService.sendToClient / broadcast.

TypeScriptService
@Injectable()
export class MyFeatureService {
  processMessage(client: Socket, dto: MyFeatureDto): void {
    client.emit('my_feature_ack', {
      received: true,
      timestamp: new Date().toISOString(),
    });
  }
}

4. Module + registro

Importe WebsocketModule e registre o handler com a factory REGISTER_HANDLERS.

TypeScriptModule
@Module({
  imports: [WebsocketModule],
  providers: [
    MyFeatureService,
    MyFeatureHandler,
    {
      provide: 'REGISTER_HANDLERS',
      useFactory: (ws: WebsocketService, handler: MyFeatureHandler) => {
        // registra o handler no bootstrap do módulo
        ws.registerMessageHandler(handler);
        return true;
      },
      inject: [WebsocketService, MyFeatureHandler],
    },
  ],
  exports: [MyFeatureService],
})
export class MyFeatureModule {}

5. AppModule

Garanta WebsocketModule.forRoot() e importe seu feature module.

Múltiplos DTOs no mesmo módulo

Padrão recomendado: um DTO e um Handler por type. Registre todos na mesma factory. O example só tem um DTO; o padrão abaixo é o esperado para features reais.

Padrão

  • • CreateXDto com type: 'x_create'
  • • UpdateXDto com type: 'x_update'
  • • CreateXHandler.canHandle === 'x_create'
  • • UpdateXHandler.canHandle === 'x_update'
  • • REGISTER_HANDLERS chama registerMessageHandler para cada um

DTOs

TypeScriptDTOs
export class ChatSendDto {
  @IsString() @IsNotEmpty()
  type: string = 'chat_send';

  @IsString() @IsNotEmpty()
  roomId!: string;

  @IsString() @IsNotEmpty()
  text!: string;
}

export class ChatTypingDto {
  @IsString() @IsNotEmpty()
  type: string = 'chat_typing';

  @IsString() @IsNotEmpty()
  roomId!: string;

  @IsBoolean()
  isTyping!: boolean;
}

Handlers

TypeScriptHandlers
@Injectable()
export class ChatSendHandler implements MessageHandler {
  canHandle(type: string) { return type === 'chat_send'; }
  async handle(client: Socket, payload: MessagePayload) {
    const dto = Object.assign(new ChatSendDto(), payload);
    // validate(dto) ...
    this.chatService.send(client, dto);
  }
}

@Injectable()
export class ChatTypingHandler implements MessageHandler {
  canHandle(type: string) { return type === 'chat_typing'; }
  async handle(client: Socket, payload: MessagePayload) {
    const dto = Object.assign(new ChatTypingDto(), payload);
    // validate(dto) ...
    this.chatService.typing(client, dto);
  }
}

Módulo

TypeScriptModule
{
  provide: 'REGISTER_HANDLERS',
  useFactory: (
    ws: WebsocketService,
    sendHandler: ChatSendHandler,
    typingHandler: ChatTypingHandler,
  ) => {
    // vários handlers no mesmo módulo
    ws.registerMessageHandler(sendHandler);
    ws.registerMessageHandler(typingHandler);
    return true;
  },
  inject: [WebsocketService, ChatSendHandler, ChatTypingHandler],
}

Módulo websocket-example

Demo mínima: um DTO type example, um handler, ack in-memory e endpoint REST de clientes online.

  • • type: 'example' + campo message obrigatório
  • • Emite websocket_example_ack após processar
  • • Emite error se a validação falhar
  • • GET /websocket-example/online — clientes ativos nos últimos 5s (JWT + Role.SUPER)
Socketexemplo de emit
socket.emit('message', {
  type: 'example',
  message: 'hello from client',
});
HTTPREST
GET /api/v1/websocket-example/online
Authorization: Bearer 

Validação

A validação de domínio fica no handler com class-validator. PayloadValidationPipe (exige type) existe em common/websocket/pipes, mas não está aplicado no gateway hoje.

  • • Handler: Object.assign(new Dto(), payload) + validate()
  • • Falha → client.emit('error', { message, errors })
  • • Pipe opcional no gateway se quiser checar type cedo

Cliente Socket.IO

Conecte com path /ws e JWT. Em seguida emita message e escute os eventos da feature.

JavaScriptsocket.io-client
import { io } from 'socket.io-client';

const socket = io('http://localhost:3000', {
  path: '/ws',
  auth: { token: accessToken },
});

socket.on('connect', () => {
  socket.emit('message', {
    type: 'example',
    message: 'hello',
  });
});

socket.on('messageResponse', (data) => {
  // { received: true }
});

socket.on('websocket_example_ack', (data) => {
  // { received: true, timestamp: '...' }
});

socket.on('error', (err) => {
  // { message: 'Validation failed', errors: [...] }
});

Abra o playground interativo em /wsui para conectar e trocar mensagens sem escrever um cliente.

/wsui

Atenção

JWT obrigatório

Sem token válido no handshake a conexão não sobe. Obtenha o access token via Auth.

Handlers async

processMessage não await o handle(). Erros async não rejeitam a Promise do gateway — trate erros dentro do handler.

Estado in-memory

O mapa de acks do example é por processo. Em múltiplas instâncias use store compartilhado (Redis, etc.).