Criando um chat com Angular, Node.js e WebSocket

Este texto tem como objetivo oferecer uma introdução ao WebSocket, destacando suas capacidades e exemplificando sua implementação na prática com Node.js e Angular.

Criando um chat com Angular, Node.js e WebSocket
Criando um chat com Angular, Node.js e WebSocket

Particularmente, acredito que uma das formas mais interessantes para estudar o uso de WebSocket é através de um projeto de bate-papo, similar a uma conversa de grupo do Whatsapp. Seguindo a mesma ideia das nostálgicas salas de bate-papo dos anos 2000, e, é claro, com uma boa simplificação, neste texto será apresentado como podemos criar um pequeno bate-papo (chat) com Angular, Node.js e WebSocket.

Funcionalidades básicas de um chat

Em um sistema de bate-papo (chat), as funcionalidades básicas são enviar e receber mensagens. Uma abordagem bem simples para o backend seria criar uma API com endpoints para enviar e receber mensagens:

  • POST /api/mensagens: envia uma mensagem para alguém.
  • GET /api/mensagens: retorna as mensagens que o usuário logado recebeu.

Já no frontend teríamos duas funções:

  • um formulário para o usuário digitar uma mensagem e enviá-la para o servidor.
  • uma função que é executada de tempos em tempos para buscar as mensagens do servidor. Este processo de requisitar algo a cada intervalo de tempo é conhecido como polling.

O Problema do Pooling

Essa abordagem tradicional, utilizando o protocolo HTTP, apresenta algumas limitações. Suponha que o frontend envie uma requisição ao servidor a cada 5 segundos para verificar se há novas mensagens; isso resulta em 12 requisições por minuto por usuário, independentemente de haver ou não mensagens para ele. Extrapolando o número de usuários para 1000, teremos 12.000 requisições por minuto, mesmo que não haja mensagens para serem recebidas.

Esse processo, conhecido como polling, é ineficiente e gera uma alta carga no servidor. Além disso, as mensagens podem sofrer um atraso considerável, pois o cliente só receberá as novas mensagens no próximo intervalo de polling.
Para resolver este problema, é mais adequado utilizar WebSocket ao invés do HTTP, por razões que serão abordadas a seguir.

WebSocket vs HTTP

No tradicional protocolo HTTP, o funcionamento é baseado em requisições e respostas, ou seja, o cliente dispara uma requisição e o servidor entrega uma resposta.

Já com WebSocket, é possível quebrarmos esse paradigma de requisições e respostas. Com ele, estabelecemos um canal de comunicação direto entre o cliente (navegador, aplicativo, etc) e o servidor, permitindo que eles troquem mensagens em tempo real e de forma bidirecional. Isso significa que o servidor pode enviar informações para você sem que você precise solicitar, e você também pode enviar informações para o servidor sem precisar esperar por uma resposta.

Dois diagramas de sequência comparando WebSocket com Http
WebSocket vs Http

Dessa forma, a utilização do WebSocket em um chat é uma abordagem totalmente viável.

WebSocket is a computer communications protocol, providing a simultaneous two-waycommunication channel over a single Transmission Control Protocol (TCP) connection. The WebSocket protocol was standardized by the IETF as RFC 6455 in 2011.

https://en.wikipedia.org/wiki/WebSocket

Agora que já abordamos o conceito do WebSocket, vamos partir para a prática e demonstrar como usá-lo na construção de um pequeno chat.

Sobre o projeto de estudo

Para simplificar o projeto, a estrutura do código foi intencionalmente ajustada, e alguns trechos assumem mais responsabilidades do que o ideal. Essa abordagem foi adotada para tornar o projeto o mais enxuto possível, focando em fins didáticos. Além disso, foram adotados alguns requisitos:

  1. A aplicação será executada em um único servidor, não estando preparada para um escalonamento horizontal nem para múltiplas instâncias;
  2. Não serão salvos os históricos das mensagens. Somente a partir do momento que um usuário se conectar na sala, ele passará a receber as mensagens;
  3. As mensagens são visíveis à todos, ou seja, não há mensagens privadas;
  4. As mensagens serão sempre no formato texto;
  5. Não será incluído nenhum meio de autenticação;

Criando o backend do chat com WebSocket e Node.js

Este pequeno projeto foi escrito em TypeScript e utilizou alguns pacotes conforme o script abaixo:

# Versão do Node.js utilizada: 20.18.0
# Inicializando o pacote NPM:
npm init -y

# Instalando as dependências
npm i ws class-validator class-transformer
npm i typescript ts-node @types/ws prettier --save-dev

# Criando o arquivo tsconfig.json
npx tsc --init

# Criando o diretório dos fontes
mkdir src

A lib ws é um pacote para implementação de WebSockets no Nodejs. Já class-validator e class-transformer são pacotes com foco em validação e conversão de objetos. Estes dois últimos, serão utilizados para validar o conteúdo que chega na API.

Após a execução do script acima, os arquivos package.json e tsconfig.json foram ajustados conforme abaixo:

{
  "name": "chat-backend",
  "version": "1.0.0",
  "main": "index.js",
  "scripts": {
    "start": "node dist/index.js",
    "start:dev": "ts-node src/index.ts",
    "build": "npx tsc --project tsconfig.json"
  },
  "author": "Marcelo R. Vismari",
  "license": "UNLICENSED",
  "engines": {
    "node": ">=20.18.0"
  },
  "dependencies": {
    "class-transformer": "^0.5.1",
    "class-validator": "^0.14.1",
    "reflect-metadata": "^0.2.2",
    "ws": "^8.18.0"
  },
  "devDependencies": {
    "@types/ws": "^8.5.12",
    "prettier": "^3.3.3",
    "ts-node": "^10.9.2",
    "typescript": "^5.6.3"
  }
}

package.json

{
  "compilerOptions": {
    "target": "es2016",
    "experimentalDecorators": true,
    "emitDecoratorMetadata": true,
    "module": "commonjs",
    "rootDir": "./src",
    "sourceMap": true,
    "outDir": "./dist",
    "removeComments": true,
    "esModuleInterop": true,
    "forceConsistentCasingInFileNames": true,
    "strict": true,
    "noImplicitAny": true,
    "strictNullChecks": true,
    "strictFunctionTypes": true,
    "strictBindCallApply": true,
    "strictPropertyInitialization": true,
    "noImplicitThis": true,
    "useUnknownInCatchVariables": true,
    "alwaysStrict": true,
    "noUnusedLocals": true,
    "noUnusedParameters": true,
    "exactOptionalPropertyTypes": true,
    "noImplicitReturns": true,
    "noFallthroughCasesInSwitch": true,
    "noUncheckedIndexedAccess": true,
    "noImplicitOverride": true,
    "noPropertyAccessFromIndexSignature": true,
    "allowUnusedLabels": true,
    "allowUnreachableCode": true,
    "skipLibCheck": true
  }
}

tsconfig.json

Padronizando as mensagens

Antes de partir para a implementação da lógica em si, vale destacar que após estabelecida uma conexão WebSocket entre cliente e servidor, a comunicação bidirecional se torna possível. Contudo, para garantir a interoperabilidade e a correta interpretação das mensagens, é fundamental estabelecer um padrão de comunicação pré-definido e conhecido por ambas as partes.

Para isto foi adotado a utilização de objetos JSON para encapsular os dados trocados entre o frontend e o backend. Além disto. para representar a estrutura das mensagens JSON, foram criadas classes específicas para mapear os dados de entrada e saída no backend.

import {
  Transform,
  TransformFnParams,
  Type,
} from 'class-transformer';
import {
  IsEnum,
  IsNotEmpty,
  IsObject,
  IsString,
  ValidateNested,
} from 'class-validator';

export enum MensagemRecebidaTipo {
  MESSAGE = 'message',
  JOIN = 'join',
}

export abstract class MsgRecebidaBase {
  @IsEnum(MensagemRecebidaTipo)
  tipo!: MensagemRecebidaTipo;
}

export class MsgRecebidaTexto extends MsgRecebidaBase {
  @IsString()
  @IsNotEmpty()
  id!: string;

  @IsString()
  @IsNotEmpty()
  @Transform(({ value }: TransformFnParams) =>
    value?.trim()
  )
  mensagem!: string;
}

export class MsgRecebidaNovoUsuario extends MsgRecebidaBase {
  @IsString()
  @IsNotEmpty()
  @Transform(({ value }: TransformFnParams) =>
    value?.trim()
  )
  nomeUsuario!: string;
}

export class TratarMsgRecebida {
  @IsObject()
  @ValidateNested()
  @Type(() => MsgRecebidaBase, {
    keepDiscriminatorProperty: true,
    discriminator: {
      property: 'tipo',
      subTypes: [
        {
          value: MsgRecebidaTexto,
          name: MensagemRecebidaTipo.MESSAGE,
        },
        {
          value: MsgRecebidaNovoUsuario,
          name: MensagemRecebidaTipo.JOIN,
        },
      ],
    },
  })
  objMensagem!:
    | MsgRecebidaTexto
    | MsgRecebidaNovoUsuario;
}

No código acima, as marcações com @ são chamadas de decorators, similares às annotations do Java e aos attributes do C#. Nesse caso, os decorators são utilizados para registrar regras de validação, permitindo que os dados recebidos pela API sejam facilmente verificados conforme as regras definidas.

Os dados que chegarão na API serão mapeados para a classe TratarMsgRecebida, que de acordo com o tipo, fará a validação para a classe MsgRecebidaNovoUsuario ou MsgRecebidaTexto.

Da mesma forma que mapeamos classes para representar dados que chegam na API, foram criadas classes para mapear mensagens que são enviadas para o frontend:

export enum MensagemSaidaTipo {
  MESSAGE = 'message',
  CONFIRMATION_RECEIPT = 'confirmacao-recebimento',
  JOIN = 'join',
  LEAVE = 'leave',
  USERS = 'users',
}

export class MsgSaida {
  constructor(public readonly tipo: MensagemSaidaTipo) {}
}

export class MsgSaidaTexto extends MsgSaida {
  constructor(
    public readonly id: string,
    public readonly remetente: string,
    public readonly mensagem: string
  ) {
    super(MensagemSaidaTipo.MESSAGE);
  }
}

export class MsgSaidaConfirmacaoRecebimento
  extends MsgSaida {
  constructor(public readonly id: string) {
    super(MensagemSaidaTipo.CONFIRMATION_RECEIPT);
  }
}

export class MsgSaidaNovoUsuario extends MsgSaida {
  constructor(public readonly nomeUsuario: string) {
    super(MensagemSaidaTipo.JOIN);
  }
}

export class MsgSaidaUsuarioSaiu extends MsgSaida {
  constructor(public readonly nomeUsuario: string) {
    super(MensagemSaidaTipo.LEAVE);
  }
}

export class MsgSaidaListaUsuarios extends MsgSaida {
  constructor(public readonly usuarios: string[]) {
    super(MensagemSaidaTipo.USERS);
  }
}

Implementação do Chat

A classe Chat, logo abaixo, contém a lógica para receber, processar e enviar mensagens. Deixei alguns comentários para facilitar o entendimento da lógica.

💡
No código a seguir, um delay de 1 segundo foi adicionado às entregas das mensagens, permitindo que, em um ambiente de estudo, seja possível visualizar a confirmação de recebimento pelo servidor.
import { plainToClass } from 'class-transformer';
import { validateOrReject } from 'class-validator';
import { WebSocket } from 'ws';
import {
  MsgRecebidaNovoUsuario,
  MsgRecebidaTexto,
  TratarMsgRecebida,
} from './models/mensagem-recebida.model';
import {
  MsgSaidaConfirmacaoRecebimento,
  MsgSaidaTexto,
  MsgSaidaListaUsuarios,
  MsgSaidaNovoUsuario,
  MsgSaida,
  MsgSaidaUsuarioSaiu,
} from './models/mensagem-saida.model';
import { Usuario } from './types/usuario.type';

export class Chat {
  conexoes = new Map<WebSocket, Usuario>();

  adicionarConexao(conexao: WebSocket) {
    // Assim que o usuário se conecta, o servidor adiciona
    // o usuário na lista de conexões com o nome
    // desconhecido. Depois esse nome é atualizado.
    this.conexoes.set(conexao, { nome: 'desconhecido' });

    // Quando o usuário envia uma mensagem para o servidor,
    // a mensagem e a processa logo abaixo:
    conexao.on('message', async (buffer) => {
      try {
        // Converte a mensagem recebida para um objeto JSON
        // e depois para a classe TratarMensagemRecebida
        const objRecebido: any = JSON.parse(
          buffer.toString('utf-8')
        );

        const mensagemRecebida = plainToClass(
          TratarMsgRecebida,
          objRecebido
        );

        // Faz a validação dos dados recebidos
        await validateOrReject(mensagemRecebida);
        const body = mensagemRecebida.objMensagem;

        // ************************************************
        // Processa as mensagens do tipo texto, ou seja,
        // mensagens de texto enviadas pelos usuários
        // ************************************************
        if (body instanceof MsgRecebidaTexto) {
          // Simula um atraso de 1 segundo
          await this.delay(1000);

          const enviarMensagemTexto = new MsgSaidaTexto(
            body.id,
            this.conexoes.get(conexao)?.nome ||
              'desconhecido',
            body.mensagem
          );
          this.enviarMensagemParaTodos(
            enviarMensagemTexto,
            conexao
          );

          // Envia a mensagem para o próprio usuário
          // confirmando que a mensagem foi recebida
          const msg = new MsgSaidaConfirmacaoRecebimento(
            body.id
          );
          this.enviarMensagem(conexao, msg);
          return;
        }

        // ************************************************
        // Processa as mensagens do tipo JOIN, ou seja,
        // mensagens que o frontend envia para informar o
        // nome do usuário que acabou de se conectar
        // ************************************************
        if (body instanceof MsgRecebidaNovoUsuario) {
          // Atualiza o nome do usuário na lista de conexões
          this.conexoes.set(conexao, {
            nome: body.nomeUsuario,
          });

          // Envia a mensagem para todos os usuários,
          // indicando que um novo usuário se conectou
          const msg = new MsgSaidaNovoUsuario(
            body.nomeUsuario
          );
          this.enviarMensagemParaTodos(msg, conexao);

          // Envia a lista de usuários conectados p/frontend
          this.enviarListaUsuarios();
          return;
        }
      } catch (error) {
        console.error(
          'Mensagem não está no formato esperado',
          error
        );
      }
    });

    // Quando o usuário desconecta do chat, o servidor
    // envia uma mensagem para todos os usuários
    // conectados, indicando que o usuário desconectou
    conexao.on('close', () => {
      const enviarMensagemUsuarioSaiu =
        new MsgSaidaUsuarioSaiu(
          this.conexoes.get(conexao)?.nome || 'desconhecido'
        );

      this.conexoes.delete(conexao);
      this.enviarMensagemParaTodos(
        enviarMensagemUsuarioSaiu,
        conexao
      );
      this.enviarListaUsuarios();
    });
  }

  /**
   * Envia uma mensagem para todos os usuários conectados,
   * exceto para o usuário que enviou a mensagem.
   *
   * @param mensagem Mensagem a ser enviada
   * @param remetente Usuário que enviou a mensagem. Se
   * não for informado, a mensagem será enviada para
   * todos os usuários
   */
  enviarMensagemParaTodos(
    mensagem: MsgSaida,
    remetente: WebSocket | null = null
  ) {
    this.conexoes.forEach((_usuario, webSocket) => {
      if (webSocket === remetente) {
        return;
      }

      this.enviarMensagem(webSocket, mensagem);
    });
  }

  /**
   * Envia uma mensagem para um usuário específico
   *
   * @param destinatario Usuário destinatário da mensagem
   * @param mensagem Mensagem a ser enviada
   */
  enviarMensagem(
    destinatario: WebSocket,
    mensagem: MsgSaida
  ) {
    destinatario.send(JSON.stringify(mensagem));
  }

  /**
   * Envia a lista de usuários conectados para todos
   * os usuários conectados
   */
  enviarListaUsuarios() {
    const enviarMensagemListaUsuarios =
      new MsgSaidaListaUsuarios(
        Array.from(this.conexoes.values()).map(
          (usuario) => usuario.nome
        )
      );

    this.enviarMensagemParaTodos(
      enviarMensagemListaUsuarios
    );
  }

  delay(ms: number) {
    return new Promise((resolve) =>
      setTimeout(resolve, ms)
    );
  }
}

chat.ts

Finalmente no arquivo index.ts, abrimos a escuta na porta 8080 e sempre que algum cliente se conectar, passamos a responsabilidade para a classe Chat:

import 'reflect-metadata';
import { WebSocketServer } from 'ws';
import { Chat } from './chat';

const porta = 8080;
const webSocketServer = new WebSocketServer({
  port: porta,
});
console.log(`Executando aplicação na porta ${porta}`);

const chat = new Chat();
webSocketServer.on('connection', (webSocket) => {
  chat.adicionarConexao(webSocket);
});

index.ts

Criando o frontend do chat com Angular 18 e Bootstrap

A implementação do frontend ficou bem enxuta, sendo dividida em 2 componentes e 1 service conforme a imagem abaixo ilustra. Além dos pacotes padrão do Angular, somente o pacote Bootstrap foi adicionado para facilitar a construção do layout.

Diagrama ilustrando a estrutura do projeto
Organização dos componentes do frontend

O código fonte ficou da seguinte forma:

💡
Não adicionei o script para criar o projeto, mas no final da página deixei o link do código fonte.

AppComponent:

import { Component, signal } from '@angular/core';
import {
  FormControl,
  FormGroup,
  ReactiveFormsModule,
  Validators,
} from '@angular/forms';
import { RouterOutlet } from '@angular/router';
import { ChatService } from './chat/chat.service';
import { ChatComponent } from './chat/chat.component';

@Component({
  selector: 'app-root',
  standalone: true,
  imports: [
    RouterOutlet,
    ChatComponent,
    ReactiveFormsModule,
  ],
  templateUrl: './app.component.html',
})
export class AppComponent {
  mostrarFormularioEntrar = signal(true);

  formGroup = new FormGroup({
    nome: new FormControl<string>('', [
      Validators.required,
      Validators.minLength(3),
    ]),
  });

  constructor(private chatService: ChatService) {}

  entrar() {
    if (this.formGroup.invalid) {
      return;
    }

    const nome = this.formGroup.get('nome')!.value!;
    this.mostrarFormularioEntrar.set(false);
    this.chatService.abrirConexao(nome);
  }
}

app.component.ts

@if (mostrarFormularioEntrar()) {
<div class="container mt-3">
  <div class="row">
    <div class="col-3"></div>
    <div class="col-6">
      <form [formGroup]="formGroup" (ngSubmit)="entrar()">
        <div class="mb-3">
          <label for="inputNome" class="form-label">
            Bem vindo ao chat consolelog.com.br
          </label>
          <input
            autofocus
            autocomplete="off"
            type="text"
            class="form-control"
            id="inputNome"
            placeholder="Digite seu nome"
            formControlName="nome" />
        </div>
        <button
          [disabled]="formGroup.invalid"
          type="submit"
          class="btn btn-primary">
          Entrar
        </button>
      </form>
    </div>
    <div class="col-3"></div>
  </div>
</div>
} @else {
<app-chat></app-chat>
}

app.component.html

ChatComponent:

import {
  AfterViewInit,
  Component,
  ElementRef,
  Signal,
  viewChild,
} from '@angular/core';
import {
  FormControl,
  FormGroup,
  ReactiveFormsModule,
  Validators,
} from '@angular/forms';
import { ChatService } from './chat.service';
import { Mensagem } from './mensagem.type';

@Component({
  selector: 'app-chat',
  templateUrl: './chat.component.html',
  standalone: true,
  imports: [ReactiveFormsModule],
})
export class ChatComponent implements AfterViewInit {
  mensagens: Signal<Mensagem[]>;
  usuariosNoChat: Signal<string[]>;
  status: Signal<string>;
  inputMensagem =
    viewChild.required<ElementRef<HTMLInputElement>>(
      'inputMensagem'
    );

  formGroup = new FormGroup({
    inputMensagem: new FormControl<string>('', {
      nonNullable: true,
      validators: [Validators.required],
    }),
  });

  get formGroupInputMensagem() {
    return this.formGroup.get('inputMensagem')!;
  }

  constructor(private chatService: ChatService) {
    this.mensagens = this.chatService.mensagens;
    this.status = this.chatService.status;
    this.usuariosNoChat = this.chatService.usuariosNoChat;
  }

  ngAfterViewInit(): void {
    this.focarInput();
  }

  focarInput() {
    this.inputMensagem().nativeElement.focus();
  }

  enviarMensagem() {
    if (this.formGroup.invalid) {
      return;
    }

    const id = crypto.randomUUID();
    const mensagem = this.formGroupInputMensagem.value;
    this.chatService.enviarMensagemTexto(mensagem, id);
    this.formGroupInputMensagem.reset();
    this.focarInput();
  }
}

chat.component.ts

<div class="container border">
  <div class="row">
    <div class="col-3">
      <div class="p-2 m-2">
        <strong>Usuários Conectados</strong>
      </div>
      @for (usuario of usuariosNoChat(); track $index) {
        <div class="p-2 m-2">
          {{ usuario }}
        </div>
      }
    </div>
    <div class="col-9">
      <div class="d-flex flex-column">
        <div
          class="p-2 flex-grow-1 d-flex flex-column"
          style="overflow-y: auto; max-height: 70vh">
          @for (mensagem of mensagens(); track $index) {
            <div class="m-2">
              @if (mensagem.tipo === 'enviada') {
                <div
                  class="p-2 border rounded d-inline-block
                         mw-75 bg-primary text-white
                         float-end">
                  {{ mensagem.mensagem }}
              
                  @if (mensagem.status === 'enviando') {
                    <i class="bi bi-check float-end"></i>
                  }
              
                  @if (mensagem.status === 'enviado') {
                    <i class="bi bi-check-all float-end"></i>
                  }
                </div>
              } @else {
                <div class="p-2 border rounded d-inline-block
                            mw-75 bg-light">
                  <strong>{{ mensagem.remetente }}:</strong>
                  {{ mensagem.mensagem }}
                </div>
              }
            </div>
          }
        </div>
        <div class="p-2">
          <form [formGroup]="formGroup"
                (ngSubmit)="enviarMensagem()">
            <div class="input-group mb-3">
              <input
                #inputMensagem
                type="text"
                class="form-control bg-light"
                formControlName="inputMensagem"
                placeholder="Digite uma mensagem" />
              <button
                [disabled]="formGroup.invalid
                            || status() !== 'conectado'"
                class="btn btn-light border"
                type="button">
                <i class="bi bi-send"></i>
              </button>
            </div>
          </form>
        </div>
        <div class="font-monospace text-end text-secondary">
          {{ status() }}
        </div>
      </div>
    </div>
  </div>
</div>

chat.component.html

ChatService:

import { Injectable, signal } from '@angular/core';
import { Mensagem } from './mensagem.type';

@Injectable({ providedIn: 'root' })
export class ChatService {
  conexao: WebSocket | null = null;
  status = signal<string>('desconectado');
  mensagens = signal<Mensagem[]>([]);
  usuariosNoChat = signal<string[]>([]);

  abrirConexao(nome: string) {
    this.status.set('conectando');
    this.conexao = new WebSocket(`ws://localhost:8080`);

    this.conexao.onopen = () => {
      this.status.set('conectado');
      this.enviarMensagemJoin(nome);
    };

    this.conexao.onmessage = (event) => {
      this.tratarMensagensRecebidas(event);
    };

    this.conexao.onclose = () => {
      this.status.set('desconectado');
    };
  }

  enviarMensagemTexto(mensagem: string, id: string) {
    this.mensagens.update((mensagens) => [
      ...mensagens,
      {
        mensagem,
        id,
        tipo: 'enviada',
        status: 'enviando',
      },
    ]);

    const payload = { tipo: 'message', mensagem, id };
    this.enviarMensagem(payload);
  }

  private enviarMensagemJoin(nomeUsuario: string) {
    const payload = { tipo: 'join', nomeUsuario };
    this.enviarMensagem(payload);
  }

  private enviarMensagem(payload: any) {
    const dados = JSON.stringify({ objMensagem: payload });
    this.conexao!.send(dados);
  }

  /**
   * Trata as mensagens recebidas do WebSocket
   * @param event Evento de mensagem recebida
   */
  private tratarMensagensRecebidas(event: MessageEvent) {
    const objMensagem = JSON.parse(event.data);

    if (objMensagem.tipo === 'message') {
      const tipo = 'recebida';
      const { remetente, mensagem } = objMensagem;

      this.mensagens.update((mensagens) => [
        ...mensagens,
        { remetente, mensagem, tipo },
      ]);
      return;
    }

    if (objMensagem.tipo === 'confirmacao-recebimento') {
      const status = 'enviado';
      const { id } = objMensagem;

      this.mensagens.update((mensagens) =>
        mensagens.map((mensagem) =>
          mensagem.id === id
            ? { ...mensagem, status }
            : mensagem
        )
      );
      return;
    }

    if (objMensagem.tipo === 'users') {
      const { usuarios } = objMensagem;
      this.usuariosNoChat.set(usuarios);
      return;
    }

    if (objMensagem.tipo === 'join') {
      const tipo = 'recebida';
      const mensagem = 'Entrou no chat.';
      const { nomeUsuario } = objMensagem;

      this.mensagens.update((mensagens) => [
        ...mensagens,
        {
          remetente: nomeUsuario,
          mensagem,
          tipo,
        },
      ]);
      return;
    }

    if (objMensagem.tipo === 'leave') {
      const tipo = 'recebida';
      const mensagem = 'Saiu no chat.';
      const { nomeUsuario } = objMensagem;

      this.mensagens.update((mensagens) => [
        ...mensagens,
        {
          remetente: nomeUsuario,
          mensagem,
          tipo,
        },
      ]);
      return;
    }
  }
}

chat.service.ts

Testando o Chat com WebSocket, Node.js e Angular

Após estruturar os dois projetos, basta executá-los e testar direto no navegador. Veja no GIF abaixo o resultado:

# Backend
npm run start:dev

# Frontend
npm run start
Dois navegadores no endereço localhost:4200 mostrando o chat em funcionamento
Resultado

Considerações

Com a possibilidade de comunicação bidirecional no uso do WebSocket, além de um chat, podemos citar vários outros exemplos, como compartilhamento de posição geográfica utilizada por exemplo no Waze e Uber, uso na tela de home brokers onde há uma taxa de atualização de informação muito frequente, jogos online, entre outros.

De forma geral, a ideia desse texto é fornecer o primeiro contato com WebSocket, mostrando sua capacidade e sua implementação na prática. Abaixo deixo um link interessante do ByteByteGo sobre o Design System de um Chat e um outro link falando sobre WebSocket:

Link do projeto:

GitHub - marcelovismari/consolelog-exemplo-chat: Código fonte utilizado para exemplificar o uso de WebSocket em um pequeno chat.
Código fonte utilizado para exemplificar o uso de WebSocket em um pequeno chat. - marcelovismari/consolelog-exemplo-chat

Código fonte mencionado neste texto