NestJS: Desenvolvendo um sistema de WebHook
WebHooks notificam para sistemas externos interessados que determinado evento ocorreu.
Em uma API REST convencional, um sistema interessado em mudanças precisaria ficar monitorando um determinado endpoint, em uma consulta agendada. Por ex: A cada 1 hora, faça um GET no endpoint e compare diferenças. Essa é uma prática desaconselhada, devido a desperdício de recursos de processamento, além do atraso entre o momento real do evento e a consulta.
Webhooks também são chamados de Callbacks ou Reverse API, visto que se trata de inverter os papéis de cada um no consumo de API: o cliente se subscreve a um evento, e cadastra uma URL para ser notificado (Endpoint REST). Sempre que esse evento ocorrer, o cliente quem receberá um POST.
As regras para o endpoint do client são as mesmas de qualquer API REST. Vale ressaltar que essa é uma comunicação server-to-server, entre backends, sem envolvimento direto do front-end.
O desenvolvimento do WebHook em si não exige regras e padrões complexos.
Simplesmente envie um POST quando algo acontecer, e chame isso de Webhook.
Contextualizando WebHook na arquitetura de microsserviços
Podemos estar nos subscrevendo para receber eventos internos de um sistema monólito, mas quando estamos lidando com microsserviços, os eventos costumam ser trafegados na rede entre si.
Esse tráfego pode ser realizado através de algum sistema de mensageria, como Kafka ou RabbitMQ. Podemos conectar o Webhook nesses sistemas para permitir a subscrição de eventos de clientes externos.
Alguém com pouco conhecimento desse tipo de arquitetura poderia argumentar que, por questões de facilidade e economia de desenvolvimento, não precisamos de Webhook, e deveríamos deixar o cliente se conectar diretamente a alguma exchange de mensagens desse sistemas, e assim, começar a receber notificações.
Ao escutar isso, contra-argumente que essa não é uma boa ideia. Sistemas de mensageria são uma parte fundamental e extremamente sensível nesse cenário, e não devem ficar expostos para redes externas. Caso contrário, estão suscetíveis à ataques, que podem impactar todos os microsserviços conectados.
Arquitetura TOTVS
Visto o tamanho da companhia, e a grande quantidade de produtos e serviços oferecidos, a arquitetura ficou um pouco mais complexa do que a apresentada acima. As principais diferenças são:
- Cada produto possui seus próprios message brokers, e o Webhook precisa se comunicar com todos esses.
- Divisão do serviço de Webhook em dois: O primeiro, efetua subscrições e controla as conexões com message brokers. O outro é responsável por enviar as notificações.
Toda essa infraestrutura está em cluster Kubernetes. A divisão desse serviço em dois foi realizada para facilitar a escalabilidade, dependendo de qual está exigindo maior processamento no momento. Apenas o serviço que permite subscrição aceita receber requisições externas.
O passo a passo de uso é o seguinte:
- Solicitar quais são os eventos disponíveis
- Realizar a subscrição em um evento, informando a URL, dados de autenticação, quantas tentativas de envio e qual o timeout.
- Receber requisições via POST na URL informada
O básico sobre NestJS
Tivemos a decisão de construir esses dois serviços utilizando o NestJS, que é um framework para construção de aplicações Node.js escaláveis (backend) com a mesma experiência e sensação de desenvolvimento do Angular (frontend).
Essa escolha foi feita pelos seguintes motivos:
- Maduro para projetos em produção
- Performático, escalável e leve
- Repositório no GitHub possui 21.7K Stars e 1.7K Forks
- Casa perfeitamente com o TypeORM, que é outro projeto sempre presente no GitHub trending topics. É um ORM para projetos typescript.
- Organização do projeto + Estrutura padrão para testes unitários
- Nosso time tem conhecimento em Angular, diminuindo a curva de aprendizado.
Para instalar o Nest CLI, e criar seu primeiro projeto, execute:
$ npm i -g @nestjs/cli
$ nest new project-name
Abaixo, encontra-se a lista dos comandos e schematics disponíveis pela Nest CLI (Para ter essa mesma visualização direto no seu terminal, basta utilizar o comando “nest”, sem nenhuma outra informação em seguida):
Perceba que existem muitas camadas similares ao Angular, tais como: módulos, controllers, services…
Evoluindo seus conhecimentos sobre NestJS
Para continuar estudando o NestJS, leia primeiramente a documentação oficial, que é muito boa e simples. Em caso de dúvidas, é possível conseguir respostas pelas issues do GitHub ou canal de discussão no Discord.
Não se contente com o básico! Para explorar o potencial desse framework e construir aplicações com qualidade alta, estude bem as camadas existentes, a função de cada uma, como os filters, interceptors, guards… tudo isso vai facilitar, e muito, o desenvolvimento quando entender.
O framework padrão de testes é o JEST, e esse também merece sua atenção individualmente.
O Projeto Webhook Queue Control and Registration
A ideia aqui é mostrar algumas partes de um projeto empresarial pronto para produção. A sua estrutura ficou da seguinte maneira:
Temos um app.module, e pastas divididas em sub módulos e seus controllers.
Exemplo de um dos módulos existentes no projeto:
Esse módulo tem a responsabilidade de auxiliar na primeira etapa: Encontrar quais são todos os eventos disponíveis em todos os message brokers conectados.
Vamos ver um controller, onde está definido o endpoint que retorna uma lista de exchanges:
Algumas coisas a notar nessa controller:
- Registramos o uso de interceptors
- Recebemos serviços via injeção de dependências
Esse interceptor irá capturar a response do endpoint, e antes de informá-lo ao usuário, irá garantir que apenas os eventos que começam com “public” estejam na lista:
Esse serviço faz requisições GET para a API do serviço de gerenciamento RabbitMQ, solicitando a lista dos exchanges disponíveis para aquele host:
O Controller “AvailableExchangesController” possui testes unitários JEST:
Ao vincular os serviços, trocamos a sua implementação real por um mock. Esse vínculo através do seguinte objeto:
{
"provide":realImplmentationClass,
"useClass": mockClass
}
E o mock:
Uma outra controller, é a que aceita os detalhes sobre uma subscrição
Note que, como esse endpoint exige input do usuário, utilizamos @UsePipes para validar a estrutura da entrada. A sua função é a de garantir que a o JSON enviado bate com o esperado pelo modelo da classe typescript. Todo endpoint que precisar fazer essa validação, simplesmente a importa através do @UsePipes.
Veja a implementação completa do pipe de validação genérico:
Outra camada genérica interessante para ter no projeto é um Exception Filter. Existe uma por default do próprio framework, mas podemos ter a nossa customizada.
Podemos registrá-lo globalmente no arquivo no main.ts
app.useGlobalFilters(new HttpExceptionFilter());
Dessa forma, sempre que alguma exceção é disparada e precisa ser retornada para o usuário (400, 500…), antes de informar a resposta, é possível executar alguma lógica. No caso da TOTVS, estamos colocando o erro no formato definido pelo guia de APIs.
Acima, verificamos códigos aplicados nesse projeto das seguintes camadas:
- Module
- Controller
- Service
- Unit Test / Mocks
- Pipes
- Exception Filter
Não utilizamos guards, visto que a autenticação e autorização estarão externas a essa aplicação (Ingress).
Com essa separação de código, garantimos que cada coisa esteja no seu lugar e não ultrapasse o limite de responsabilidade.
Futuro
A intenção é que em um futuro próximo, essa aplicação esteja disponível para uso dos nossos clientes.
No momento, esse projeto se encontra em repositório privado, mas temos a intenção de abrir o fonte. Vamos migrar o projeto para o GitHub, para nos posicionarmos mais ativamente na comunidade do NestJS!