Microsserviços robustos com NestJS

Caio Ferreira
b2w engineering
Published in
8 min readAug 21, 2020

A B2W atualmente opera um amplo ecossistema de microsserviços que precisa comportar as múltiplas demandas de negócios e, ao mesmo tempo, suportar o tráfego de eventos como a Black Friday. Por isso, construir aplicações cada vez mais robustas é uma busca constante.

O NestJS surge como uma alternativa de framework progressivo (escrito em Typescript), extensível e versátil. Para discutir como ele pode auxiliar na construção de microserviços robustos, é preciso primeiramente definir o conceito de robustez.

Como se caracteriza um microsserviço robusto?

Em termos simples, um microsserviço robusto é modular e escalável.

Primeiramente ele deve ser modular, pois uma arquitetura de microsserviços é naturalmente evolutiva. Durante toda sua existência, serão apresentados novos desafios e a necessidade de reorganizar suas aplicações. Por isso, ter a capacidade de extrair os módulos em novos serviços é fundamental para a manutenção do sistema. Mesmo com toda a evolução de uma arquitetura sendo desafiadora, é na modularidade da aplicação que esta tarefa se torna viável.

Paralelamente, é necessário que a aplicação seja escalável.

Escalabilidade horizontal, monitoria e non-blocking IO são características comuns no ambiente NodeJS. Para que uma solução escale, é necessária a utilização correta das ferramentas para realizar o trabalho.

Um serviço robusto deve ter as regras do seu domínio isoladas do ferramental de comunicação (REST/gRPC/GraphQL), persistências (SQL/NoSQL), caching, logging, monitoria e ser capaz de escolher essas ferramentas conforme as necessidades de performance.

O que é o NestJS?

Trata-se de um framework lançado em 2017, hoje na versão 7.0.11 (no momento em que este artigo é escrito). Durante sua existência, o NestJS construiu uma sólida comunidade, contando com diversos apoiadores pessoais e corporativos, inclusive sendo o repositório Typescript com o maior crescimento de 2018 em termos de estrelas no Github (280%).

Um dos projetos sobre o qual o NestJS construiu suas fundações é o Express, usado como padrão para fornecer uma interface HTTP a aplicações criadas com o framework. Isso permite utilizar todo o conhecimento sobre operação de aplicações e ferramental do Express.

Outro projeto de grande influência no NestJS é o Angular, observado no sistema de injeção de dependências, forte uso de annotations em sua API e codebase escrita em Typescript.

Modularidade

A fim de compreender como o NestJS pode ajudar a construir serviços modulares, é preciso conhecer as abstrações que compõem o sistema mais importante dentro do framework, o Container de Injeção de Dependências.

O que é um Container de Injeção de Dependências?

Quem possui conhecimento em Java ou C# achará este um conceito bem familiar, mesmo não sendo muito difundido em frameworks/libs dentto do ecossistema NodeJS.

Quando se desenha e implementa qualquer classe, objetiva-se que ela tenha apenas uma responsabilidade (Single Responsability Principle), realizando e encapsulando um conhecimento, algoritmo ou regra de negócio. Porém, muitas vezes esse aprendizado é sobre como compor outras regras.

É possível citar um middleware que controla o acesso de um usuário a um recurso de uma aplicação e que precisa compor a regra de autorização com o conhecimento para acessar um banco de dados, usando as informações do usuário.

Para isso, essa classe necessita uma entidade que contenha a regra de autorização e outra que encapsule o conhecimento sobre como acessar o banco de dados. Neste cenário, dizemos que o middleware depende da classe de autorização e da classe de acesso ao banco.

Um Container de Injeção existe exatamente para facilitar o acesso de uma classe a suas dependências. A boa prática é que elas sejam declaradas como um argumento do construtor da classe.

No NestJS, o Container cria uma instância de cada classe na inicialização da aplicação, de forma que os argumentos dos construtores de cada classe sejam providos ele próprio. Esta é uma estrutura muito parecida com outros frameworks, como o Spring.

Porém, como o Nest descobre qual classe precisa ser gerenciada pelo Container e qual instância deve ser injetada no construtor de outra classe? Através dos Providers.

Injeção de Dependências no NestJS

Um Provider é simplesmente um objeto javascript que associa um token a um valor. Todos os providers são gerenciados pelo Container e podem depender de outros providers. Qualquer coisa que seja um valor no javascript pode ser gerenciado pelo Container, seja uma string, um número, um objeto, uma instância ou uma função.

Existem duas formas para se declarar um provider: standard (padrão) e custom (customizada).

A declaração standard de um provider é apenas um syntax sugar que usa uma annotation @Injectable, indicando que aquela classe pode requerer dependências e pode ser injetada em outros providers/classes. Nesse cenário, o token utilizado é o próprio tipo da classe.

https://gist.github.com/dedb13ed69b34eb4f010dc7b162fb656

Por outro lado, é possível usar providers customizados. Com eles, é possível escolher o token (geralmente uma string ou Symbol) e definir o valor associado de três maneiras diferentes, com useValue/useFactory/useClass.

Cada um permite definir o provider como for mais conveniente. Por exemplo, useFactory é extremamente útil para cenários onde o desejo é encapsular uma dependência externa que precisa de parâmetros dinâmicos, como uma conexão de banco de dados.

https://gist.github.com/368aa3b1d08a3845cae043b32dcf123b

A “exceção” a esta regra são os Controllers, que não podem ser injetados como dependências em outras classes e tem apenas uma maneira de serem declarados.

https://gist.github.com/6323c3b5d2e635150f0189eeeaee3b47

Mas como uma classe declara que depende desses providers? Existem duas maneiras: se o Custom Provider estiver sendo usado, é necessário declarar um parâmetro no construtor da classe e decorá-lo com a annotation @Inject, informando o token do Provider. Para o cenário dependente de um Standard Provider, basta declarar um argumento cujo tipo seja o de uma classe declarada com a annotation @Injectable.

https://gist.github.com/7d9c6aa409be5544c1702279b097333b

Contudo, esta estrutura deixa em aberto duas questões:

  1. Como o NestJS acha esses providers e os registra no container?
  2. Se todos os providers fizerem parte do contexto global do container, como podemos isolar providers úteis em apenas um contexto? Como permitir que classes dependam apenas de um UserRepository e não de um provider como MongoUserRepository que fornece uma implementação específica?

Neste momento é que pode-se utilizar o conceito de Módulos.

Organizando a aplicação em Módulos

Módulos são responsáveis por registrarem controllers e providers, fornecendo uma abstração para empacotar implementações relacionadas, geralmente representando uma feature da sua aplicação. Como outras abstrações no NestJS, um módulo nada mais é do que uma classe com uma annotation @Module.

Essa annotation recebe um objeto como argumento, listando todos os providers pertencentes ao módulo, assim como os controllers. Um detalhe importante é que todo provider é privado ao módulo, isto é, somente providers registrados no mesmo módulos podem requisitá-los como dependência.

https://gist.github.com/ae183243bc7de306d1ba6684dacb3fcb

O que isso implica? Observe o StringController,ele depende de CLIENT_KEYS, porém o provider para esse token está registrado no UtilModule, separado do controller. Nesse cenário, haveria uma falha na inicialização da aplicação, pois o NestJS não seria capaz de resolver as dependências do controller.

A forma mais simples de resolver isso é importando o UtilsModule no módulo que registra o controller. Quando o módulo é importado, ganha-se acesso a todos os providers que são explicitamente exportados.

Ao se importar o UtilsModule no módulo do controller, o StringController terá acesso apenas ao ClientKeysProvider, pois ele está presente no parâmetro exports do UtilsModule.

https://gist.github.com/98aac25b3478fba0f60998d83d90f945

Finalmente, é preciso importar o StringModule no módulo raiz AppModule. Dessa maneira, quando o NestJS varrer a árvore de módulos também encontrará o UtilsModule, pois trata-se de uma dependência do StringModule.

https://gist.github.com/b1c4a76fac16bf1f12b704dcc05d8b05

Obs: Uma nota importante é que os controllers são entidades especiais, públicos e todas as suas rotas declaradas são cadastradas na instância Express final.

Baseado nesse sistema de injeção de dependências, o NestJS apresenta uma visão de arquitetura desacoplada que não apenas favorece a construção de modularidade dentro de um serviço, como também é a semente de uma estratégia de otimização que produz aplicações mais escaláveis.

Escalabilidade

O NestJS leva sua filosofia de modularidade a um outro patamar, apresentando uma visão de “plataforma”. Tudo que não for estritamente necessário para a resolução do seu sistema de injeção de dependências é tratado como um módulo “plugável”.

Este conceito traz a possibilidade de experimentar e ajustar o ferramental conforme as necessidades de escalabilidade.

Usando a plataforma nativa

Tecnologias suportadas nativamente por módulos do NestJS

O NestJS é construído sobre uma engine HTTP confiável e battle tested, o Express. Devido a sua simplicidade, o Express tem gargalos de performance que dependem do volume e latência demandados.

Por isso nasceu como alternativa o Fastify, um framework focado em diminuir o máximo possível o overhead introduzido pelo servidor.

Devido a capacidade do NestJS de construir abstrações, a plataforma HTTP da qual ele depende também é um detalhe de implementação que pode ser alterado substituindo o @nestjs/platform-express por@nestjs/platform-fastify, modificando a configuração do entrypoint da aplicação.

Para atingir as demandas de escala, é necessário escapar do simples caminho de uma API REST e lançar mão de protocolos e arquiteturas otimizadas.

Pensando nisso, o NestJS oferece suporte nativo a tecnologias como GraphQL, gRPC e NATS, integração com ferramentas como ORMs para bancos SQL e NoSQL, como o TypeORM e o Mongoose, entre outros módulos.

Com isso, é possível evoluir uma aplicação para utilizar novas soluções, apenas substituindo os módulos nas bordas do serviço.

Expandindo a plataforma

Todo microsserviço necessita de uma boa cobertura de logging, que permita aos seus desenvolvedores a investigação de problemas em produção e que possa produzir insights com os dados registrados.

Seguindo a filosofia de modularidade e abstração do NestJS em uma aplicação que estava em fase inicial de desenvolvimento, foi implementado um módulo para encapsular a plataforma de logging que a aplicação usaria, o Winston, a biblioteca mais popular para log no ecossistema NodeJS.

Contudo, durante investigações em outra aplicação que usava Express puro, percebeu-se que o Winston estava sendo responsável por longas pausas no Event Loop, acarretando um grande consumo de CPU e alta latência.

Diante do que constatou, optou-se por substituí-lo pelo PinoJS, uma alteração que levou a uma redução de quase 10% no consumo de CPU e quase 80ms na latência.

Com esta conclusão, para replicar a substituição na aplicação NestJS foi necessário apenas “desplugar” um módulo e substituí-lo por uma nova implementação.

Esta é uma demonstração da porder do conceito de modularidade e como ele não deve ser restrito apenas às ferramentas nativamente fornecidas pelo framework.

Antes, os próprios desenvolvedores das aplicações devem seguir o exemplo e construir uma plataforma de ferramentas modulares que possa ser otimizada conforme as demandas de performance.

Conclusão

NestJS é um framework que se propõe a ser uma plataforma que provê a infraestrutura necessária para que novas funcionalidades sejam criadas sobre ele.

Isso permite que ele forneça uma gama de módulos nativos, facilitando o uso de soluções populares, sem limitar os desenvolvedores de trazerem novas ferramentas com a mesma modularidade daquelas suportadas nativamente.

Logo, o NestJS se apresenta como um framework maduro e capaz de sustentar uma estratégia bem sucedida de microserviços no ecossistema do NodeJS.

--

--

Caio Ferreira
b2w engineering

Developer @ B2W. Paixonate by functional programming and software architecture.