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 }
// 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
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
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
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.
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.
@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.
@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.
@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
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
@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
{
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)
socket.emit('message', {
type: 'example',
message: 'hello from client',
});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.
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.
/wsuiAtençã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.).