Aprenda a criar uma API de bate-papo em tempo real aproveitando o poder do WebSockets usando o NestJS.

NestJS é uma estrutura popular para criar aplicativos do lado do servidor com Node.js. Com seu suporte para WebSockets, o NestJS é adequado para desenvolver aplicativos de bate-papo em tempo real.

Então, o que são WebSockets e como você pode criar um aplicativo de bate-papo em tempo real no NestJS?

O que são WebSockets?

WebSockets são um protocolo para comunicação persistente, em tempo real e bidirecional entre um cliente e um servidor.

Ao contrário do HTTP, onde uma conexão é fechada quando um ciclo de solicitação é concluído entre o cliente e o servidor, uma conexão WebSocket é mantida aberta e não fecha mesmo depois que uma resposta foi retornada para um solicitar.

A imagem abaixo é uma visualização de como funciona uma comunicação WebSocket entre um servidor e um cliente:

Para estabelecer comunicação bidirecional, o cliente envia uma solicitação de handshake WebSocket ao servidor. Os cabeçalhos de solicitação contêm uma chave WebSocket segura (

Sec-WebSocket-Key), e um Atualização: WebSocket cabeçalho que juntamente com o Conexão: atualização header informa ao servidor para atualizar o protocolo de HTTP para WebSocket e manter a conexão aberta. Aprendendo sobre WebSockets em JavaScript ajuda a entender ainda melhor o conceito.

Construindo uma API de bate-papo em tempo real usando o módulo NestJS WebSocket

O Node.js fornece duas implementações principais de WebSockets. O primeiro é ws que implementa WebSockets simples. E o segundo é soquete.io, que fornece mais recursos de alto nível.

O NestJS possui módulos para ambos soquete.io e ws. Este artigo usa o soquete.io módulo para os recursos de WebSocket do aplicativo de amostra.

O código utilizado neste projeto está disponível em um Repositório GitHub. É recomendável cloná-lo localmente para entender melhor a estrutura de diretórios e ver como todos os códigos interagem entre si.

Configuração e instalação do projeto

Abra seu terminal e gere um novo aplicativo NestJS usando o ninho novo comando (por exemplo aninhar novo aplicativo de bate-papo). O comando gera um novo diretório que contém os arquivos do projeto. Agora você está pronto para iniciar o processo de desenvolvimento.

Configurar uma conexão MongoDB

Para persistir as mensagens de chat no aplicativo, você precisa de um banco de dados. Este artigo usa o banco de dados MongoDB para nosso aplicativo NestJS, e a maneira mais fácil de começar a correr é configurar um cluster MongoDB na nuvem e obtenha sua URL do MongoDB. Copie o URL e armazene-o como o MONGO_URI variável em seu .env arquivo.

Você também precisaria do Mongoose mais tarde, quando fizer consultas ao MongoDB. Instale-o executando npm instalar mangusto em seu terminal.

No origem pasta, crie um arquivo chamado mongo.config.ts e cole o seguinte código nele.

importar { registrar Como } de'@nestjs/config';

/**
* Configuração de conexão do banco de dados Mongo
*/

exportarpadrão registrarAs('mongodb', () => {
const { MONGO_URI } = processo.env; // do arquivo .env
retornar {
uri:`${MONGO_URI}`,
};
});

do seu projeto main.ts arquivo deve ficar assim:

importar { NestFactory } de'@nestjs/core';
importar { AppModule } de'./app.module';
importar * como cookieParser de'analisador de cookies'
importar capacete de'capacete'
importar { Logger, ValidationPipe } de'@nestjs/common';
importar { setupSwagger } de'./utils/swagger';
importar { HttpExceptionFilter } de'./filters/http-exception.filter';

assíncronofunçãobootstrap() {
const aplicativo = aguardam NestFactory.create (AppModule, { núcleos: verdadeiro });
app.enableCors({
origem: '*',
credenciais: verdadeiro
})
app.use (cookieParser())
app.useGlobalPipes(
novo ValidationPipe({
lista branca: verdadeiro
})
)
const registrador = novo Registrador('Principal')

app.setGlobalPrefix('api/v1')
app.useGlobalFilters(novo HttpExceptionFilter());

setupSwagger (aplicativo)
app.use (capacete())

aguardam app.listen (AppModule.port)

// registra documentos
const baseUrl = AppModule.getBaseUrl (aplicativo)
const URL = `http://${baseUrl}:${AppModule.port}`
logger.log(`Documentação da API disponível em ${url}/docs`);
}
bootstrap();

Construindo o módulo de bate-papo

Para começar a usar o recurso de bate-papo em tempo real, a primeira etapa é instalar os pacotes NestJS WebSockets. Isso pode ser feito executando o seguinte comando no terminal.

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

Depois de instalar os pacotes, você precisa gerar o módulo de chats executando os seguintes comandos

bate-papos do módulo nest g
bate-papos do controlador Nest G
bate-papos do serviço Nest G

Terminada a geração do módulo, o próximo passo é criar uma conexão WebSockets no NestJS. Criar uma chat.gateway.ts arquivo dentro do chats pasta, é onde está implementado o gateway que envia e recebe mensagens.

Cole o seguinte código em chat.gateway.ts.

importar {
Corpo da mensagem,
Inscrever-seMensagem,
WebSocketGateway,
WebSocketServer,
} de'@nestjs/websockets';
importar { Servidor } de'socket.io';

@WebSocketGateway()
exportaraulaChatGateway{
@WebSocketServer()
servidor: Servidor;
// escuta eventos send_message
@SubscribeMessage('enviar mensagem')
listenForMessages(@MessageBody() mensagem: string) {
esse.server.sockets.emit('receber_mensagem', mensagem);
}
}

Autenticação de usuários conectados

A autenticação é uma parte essencial dos aplicativos da Web e não é diferente para um aplicativo de bate-papo. A função para autenticar as conexões do cliente ao soquete é encontrada em chats.service.ts conforme mostrado aqui:

@Injetável()
exportaraulaChatsServiço{
construtor(private authService: AuthService) {}

assíncrono getUserFromSocket (soquete: Soquete) {
deixar auth_token = socket.handshake.headers.authorization;
// obtém o próprio token sem "Bearer"
auth_token = auth_token.split(' ')[1];

const usuário = esse.authService.getUserFromAuthenticationToken(
auth_token
);

se (!do utilizador) {
lançarnovo WsException('Credenciais inválidas.');
}
retornar do utilizador;
}
}

O getUserFromSocket método usa getUserFromAuthenticationToken para obter o usuário conectado no momento do token JWT extraindo o token Bearer. O getUserFromAuthenticationToken função é implementada no auth.service.ts arquivo como mostrado aqui:

público assíncrono getUserFromAuthenticationToken (token: string) {
const carga útil: JwtPayload = esse.jwtService.verify (token, {
segredo: esse.configService.get('JWT_ACCESS_TOKEN_SECRET'),
});

const userId = payload.sub

se (ID do usuário) {
retornaresse.usersService.findById (userId);
}
}

O soquete atual é passado como um parâmetro para getUserFromSocket quando o handleConnection método de ChatGateway implementa o OnGatewayConnection interface. Isso permite receber mensagens e informações sobre o usuário atualmente conectado.

O código abaixo demonstra isso:

// chat.gateway.ts
@WebSocketGateway()
exportaraulaChatGatewayimplementosOnGatewayConnection{
@WebSocketServer()
servidor: Servidor;

construtor(Serviço de bate-papo privado: Serviço de bate-papo) {}

assíncrono handleConnection (soquete: Soquete) {
aguardamesse.chatsService.getUserFromSocket (soquete)
}

@SubscribeMessage('enviar mensagem')
assíncrono listenForMessages(@MessageBody() message: string, @ConnectedSocket() socket: Socket) {

const usuário = aguardamesse.chatsService.getUserFromSocket (soquete)
esse.server.sockets.emit('receber_mensagem', {
mensagem,
do utilizador
});
}
}

Você pode fazer referência aos arquivos envolvidos no sistema de autenticação acima no Repositório GitHub para ver os códigos completos (inclusive importados), para melhor entendimento da implementação.

Persistindo chats no banco de dados

Para que os usuários vejam seu histórico de mensagens, você precisa de um esquema para armazenar mensagens. Crie um novo arquivo chamado mensagem.esquema.ts e cole o código abaixo nele (lembre-se de importar seu esquema de usuário ou verifique o repositório para um).

importar { Do utilizador } de'./../users/schemas/user.schema';
importar { Prop, Schema, SchemaFactory } de"@nestjs/mangusto";
importar mangusto, { Documento } de"mangusto";

exportar digite MessageDocument = Mensagem & Documento;

@Esquema({
para JSON: {
getters: verdadeiro,
virtuais: verdadeiro,
},
carimbos de data/hora: verdadeiro,
})
exportaraulaMensagem{
@Suporte({ obrigatório: verdadeiro, exclusivo: verdadeiro })
mensagem: string

@Suporte({ tipo: mangusto. Esquema. Tipos. ObjectId, ref: 'Do utilizador' })
usuário: usuário
}

const MessageSchema = SchemaFactory.createForClass (Mensagem)

exportar { Esquema da Mensagem };

Abaixo está uma implementação de serviços para criar uma nova mensagem e obter todas as mensagens em chats.service.ts.

importar { Mensagem, MensagemDocumento } de'./message.schema'; 
importar { Soquete } de'socket.io';
importar { AuthService } de'./../auth/auth.service';
importar { Injetável } de'@nestjs/common';
importar { WsException } de'@nestjs/websockets';
importar {InjectModel} de'@nestjs/mangusto';
importar { Modelo } de'mangusto';
importar { MensagemDpara } de'./dto/mensagem.dto';

@Injetável()
exportaraulaChatsServiço{
construtor(private authService: AuthService, @InjectModel (Message.name) private messageModel: Model) {}
...
assíncrono createMessage (mensagem: MessageDto, ID do usuário: corda) {
const novaMensagem = novoesse.messageModel({...message, userId})
aguardam novaMensagem.salvar
retornar nova mensagem
}
assíncrono getAllMessages() {
retornaresse.messageModel.find().populate('do utilizador')
}
}

O MessageDto é implementado em um mensagem.dto.ts arquivo no dto pasta no chats diretório. Você também pode encontrá-lo no repositório.

Você precisa adicionar o mensagem modelo e esquema à lista de importações em chats.module.ts.

importar { Mensagem, MessageSchema } de'./message.schema';
importar { Módulo } de'@nestjs/common';
importar { ChatGateway } de'./chats.gateway';
importar {ChatsServiço} de'./chats.service';
importar { MongooseModule } de'@nestjs/mangusto';

@Módulo({
importações: [MongooseModule.forFeature([
{ nome: Message.name, esquema: MessageSchema }
])],
controladores: [],
provedores: [ChatsService, ChatGateway]
})
exportaraulaMódulo Chats{}

finalmente, o get_all_messages manipulador de eventos é adicionado ao ChatGateway classe em chat.gateway.ts como visto no código a seguir:

// importa...

@WebSocketGateway()
exportaraulaChatGatewayimplementosOnGatewayConnection{
...

@SubscribeMessage('get_all_messages')
assíncrono getAllMessages(@ConnectedSocket() soquete: Soquete) {

aguardamesse.chatsService.getUserFromSocket (soquete)
const mensagens = aguardamesse.chatsService.getAllMessages()

esse.server.sockets.emit('receber_mensagem', mensagens);

retornar mensagens
}
}

Quando um cliente (usuário) conectado emite o get_all_messages evento, todas as suas mensagens serão recuperadas, e quando elas emitirem enviar mensagem, uma mensagem é criada e armazenada no banco de dados e, em seguida, enviada para todos os outros clientes conectados.

Depois de concluir todas as etapas acima, você pode iniciar seu aplicativo usando npm run start: deve teste-o com um cliente WebSocket como o Postman.

Construindo aplicações em tempo real com NestJS

Embora existam outras tecnologias para a construção de sistemas de tempo real, os WebSockets são muito populares e fáceis de implementar em muitos casos, sendo a melhor opção para aplicações de chat.

Os aplicativos em tempo real não se limitam apenas aos aplicativos de bate-papo, outros exemplos incluem streaming de vídeo ou aplicativos de chamada e aplicativos de clima ao vivo, e o NestJS fornece ótimas ferramentas para criar aplicativos.