Arquitetura de micro serviços em Node.js + MongoDB: Parte 3

E chegamos à terceira parte da nossa série de artigos sobre como implementar na prática um projeto de sistema usando arquitetura de microservices usando Node.js e MongoDB.

Na primeira parte desta série eu dei uma introdução teórica sobre microservices e porque você deveria estar olhando para esta arquitetura e principalmente para as tecnologias Node e Mongo para implementá-la. Finalizei este artigo dando um case de exemplo que usaríamos para desenvolvimento ao longo dos outros artigos.

Na segunda parte, começamos a estruturar nosso projeto, definindo camadas, serviços e responsabilidades. Na sequência modelamos o nosso banco de dados, criamos o módulo de conexão e o módulo de acesso a dados (repositório), tudo isso usando configurações através de variáveis de ambiente (com dotenv-safe) e testes unitários com Tape.

Nesta terceira parte vamos finalmente finalizar o desenvolvimento do nosso primeiro microservice, o movie-service, que fornecerá acesso a consultas de filmes por ID, filmes que são lançamento e todos os filmes de maneira genérica. Lembrando que este serviço será utilizado por outro que fará a interface com a aplicação propriamente dita, como ilustrado pelo diagrama abaixo.

Consulta de Lançamentos do Cinema
Consulta de Lançamentos do Cinema

Então mãos à obra!

Programando o servidor

Agora é hora de programarmos os comportamentos da nossa API, mas antes disso precisamos construir nosso servidor.

Qualquer um que já leu alguma coisa a respeito na Internet sabe que este é o ponto forte do Node.js. É possível construir servidores web muito facilmente com Node a partir de 12 linhas de código.

Como cada micro serviço deve rodar standalone, sem depender de outros, é extremamente interessante que cada um tenha o seu próprio módulo de server.js para ser instanciado isoladamente.

O conteúdo do server.js pode ser visto abaixo:

Este servidor é genérico e simples, com uma função para iniciá-lo e outra para encerrá-lo. Ele usa o pacote morgan para logging de requisições no terminal/console e o helmet para garantir a proteção contra 11 ataques diferentes que sua API pode sofrer quando ir para produção e estar à mercê de hackers.

A função start espera a api, que vamos construir na sequência, o repositório, que já construímos e um callback que é disparado após a inicialização do servidor ser concluída. A api em si é que faz a magia de definição e tratamento das requisições em rotas específicas, tal qual já fizemos em outros tutoriais de Express aqui do blog.

Seguindo a nossa linha de ter unit tests para cada módulo do nosso projeto, vamos criar dentro da pasta cinema-microservice/movies-service/src/server um arquivo server.test.js contendo os testes abaixo:

Nestes testes nós iniciaremos o servidor usando uma API mockada (fake) e depois encerraremos este mesmo servidor. Bem simples, apenas para saber se ele está de fato subindo e sendo encerrado com sucesso.

Você pode rodar este teste isoladamente executando este arquivo com o comando ‘node server.test’ ou então adicionar uma nova linha no nosso índice de testes:

E com isso você já consegue garantir também que nosso servidor está funcionando, rodando um ‘npm test’ no console e vendo os resultados de todos os testes criados até o momento.

Testes de Servidor Ok
Testes de Servidor Ok

E agora, vamos finalmente criar a API em si?

Programando a API

Agora que temos o banco de dados, o repositório, o servidor e uma bateria de unit tests garantindo que tudo está funcionando como deveria, é hora de programarmos a API.

Na arquitetura desenhada até o momento, temos o arcabouço de servidor que espera que seja plugado um repositório e uma API. O repositório já temos pronto e a API vamos criar agora, dentro do que o servidor espera.

Para isso, dentro da pasta cinema-microservice/movies-service/src/api, crie um arquivo movies.js com o conteúdo abaixo:

Note que uma vez que grande parte do trabalho já foi segregado em outros módulos, coube ao módulo da API em si uma responsabilidade bem específica e ao mesmo tempo pequena, que é o tratamento das requisições.

Para garantir que esta nossa API está funcionando, vamos criar um movies.test.js na mesma pasta api para criarmos os testes abaixo:

Esse arquivo de teste ficou bem complicado, afinal, para conseguir testar nossa API temos de subir um servidor, conectar o repositório ao banco e usar uma biblioteca chamada supertest (não esqueça de rodar um NPM install) para simular as chamadas HTTP e com isso verificar se tudo está sendo retornado nos três endpoints como deveria.

Para garantir que os testes só vão rodar após o servidor ter subido, coloquei os testes dentro do callback do server.listen. Não vou entrar em detalhes do supertest aqui pois já falei dele no post de TDD em Node. Adicione mais uma linha no arquivo index.test.js e rode com um npm test para ver o resultado com seus próprio olhos.

API Ok
API Ok

Agora que temos a nossa API pronta e funcionando, vamos atar tudo no arquivo index.js do projeto movies-service.

Programando o Index

Como última etapa para fazer a nossa API de filmes funcionar, temos de orquestrar todos os módulos que compõem a API no arquivo index.js, pois é ele que será chamado para startar o nosso microsserviço quando colocarmos ele em um servidor (preferencialmente em um container Docker, mas isso é outra história).

Na verdade, uma vez que chegamos até aqui, com essa quantidade de testes e arquitetura de micro serviços bem definida, o index.js ficará tão simples quanto abaixo:

Se você duvida que é apenas isso, experimente rodar este index através de ‘node index’ ou ‘npm start’ e você vai poder testar, seja via POSTMAN ou diretamente no navegador mesmo.

Api funcionando no navegador
Api funcionando no navegador

Algumas boas práticas relacionadas a construção de APIs RESTful podem ser encontradas neste ótimo post (em inglês), mas te garanto que com esse basicão que vimos aqui você já está bem preparado para construir coisas mais poderosas e em cenários reais.

E não se preocupe, com o que lhe mostrei neste artigo conseguimos concluir apenas o primeiro microservice do nosso sistema completo. Quero que no mínimo consigamos avançar para o funcionamento do segundo microservice para fazer o fluxo básico de consultar filmes em cartaz em um cinema específico de uma cidade.

Mas isso fica para o próximo post da série!

Curtiu o post? Então clica no banner abaixo e dá uma conferida no meu livro sobre programação web com Node.js!

Arquitetura de micro serviços em Node.js + MongoDB: Parte 2

No primeiro artigo desta série eu fiz um resumão do porque escolher uma arquitetura de micro serviços para seus sistemas vale a pena, quais as vantagens do modelo e indiquei Node e Mongo como uma dupla de tecnologias a serem consideradas para este tipo de abordagem. Finalizei o artigo passado explicando a arquitetura de um case de exemplo envolvendo um sistema para uma rede de cinemas.

Neste artigo continuaremos a série, mas desta vez colocando a mão na massa: organizaremos a estrutura padrão que será usada em nossos microservices, construiremos o primeiro deles e modelaremos o seu banco de dados.

Então vamos lá!

Organizando a arquitetura

Relembrando rapidamente o primeiro cenário de uso da nossa arquitetura de microservices:

Consulta de Lançamentos do Cinema
Consulta de Lançamentos do Cinema

Neste cenário, iniciaremos nosso desenvolvimento com o microservice MOVIES e sua respectiva database. Cabe a esse serviço fornecer informações referentes ao catálogo de filmes cujos direitos de exibição foram comprados pela rede. Além do CRUD básico, espera-se deste serviço que seja possível saber quais filmes são os lançamentos da rede, basicamente os que entraram nos últimos 30 dias, que é mais ou menos a duração do status de lançamento de um filme.

Para estruturar este projeto como um todo, crie uma pasta central chamada cinema-microservice. Dentro dela colocaremos todos os microservices e dados dos mesmos, divididos em subpastas, por uma questão de organização, como mostra a hierarquia de pastas abaixo.

  • cinema-microservice
    • movies-service
      • data
      • src
    • cinema-catalog-service
      • data
      • src

Obviamente quando fizermos o deploy dos mesmos, eles serão feitos de maneira independente, mas por uma questão de organização do projeto e do repositório se você vier a versionar este projeto, faz sentido agrupá-los desta forma. Apenas lembre-se de não versionar as pastas de dados e a node_modules de cada microservice, adicionando os respectivos caminhos no seu .gitignore.

Dentro da subpasta movies-service, que é a que vamos focar neste artigo, temos as pastas data e src. Na pasta data armazenaremos os dados do nosso banco MongoDB (basta apontar o dbpath para cá na inicialização do banco) deste microservice. Já na pasta src armazenaremos os códigos-fonte do mesmo.

Dentro da pasta src teremos a seguinte estrutura de pastas e arquivos, em todos os nossos microservices a partir deste aqui:

  • movies-service
    • src
      • api
      • config
      • repository
      • server
      • index.js
      • packages.json

Os arquivos index.js e packages.json são auto-explicativos no cenário de uma webapi em Node.js. Na pasta api teremos os módulos das rotas deste microservice. Na pasta config, os módulos de configuração e de acesso básico a dados (MongoDB cru). Na pasta repository nós teremos módulos seguindo o pattern Repository, uma versão mais “NoSQL” do pattern DAO (Data Access Object, focado em SQL).

E basicamente esta é a estrutura, agora vamos aos dados!

Organizando os dados

Como estamos focando no microservice MOVIES, nossa base de dados será bem tranquila pois teremos apenas uma coleção de documentos com todos os filmes dentro. Obviamente se você não está acostumado com modelagem de dados em MongoDB (se é que modelagem é o termo correto aqui), sugiro a série de artigos MongoDB para iniciantes em NoSQL e até mesmo o meu livro de MongoDB.

Nossos filmes possuem a seguinte informação:

  • identificador único
  • título
  • duração (em minutos)
  • imagem (capa promocional)
  • sinopse
  • data de lançamento
  • categorias (ação, romance, etc)

Obviamente você deve imaginar que poderíamos ter muitas outras informações aqui como faixa etária, trailer, formato de tela, idioma, etc. Vou ficar só com essas por uma questão de simplicidade.

Em um banco relacional tradicional, como isso seria modelado? Algumas colunas da suposta tabela Filmes são bem óbvias como ID, Titulo, Duracao, Sinopse e DataLancamento. Mas e o campo imagem? Apesar dos bancos SQL suportarem BLOBs, nunca foi uma boa opção por pesar demais nos SELECTs e no crescimento do banco como um todo. No entanto, o mesmo não pode ser dito do MongoDB, onde podemos ter campos binários facilmente sem abrir mão da performance. Ainda assim entenderei se você decidir por armazenar apenas a URL da imagem em uma URL pública (AWS S3?).

Mas o que eu queria falar mesmo era das categorias. Esta é uma relação que pelas formas normais  e levando muito a sério a não-repetição dos dados deveria ser N-N com 3 tabelas: uma Filmes, outra Categorias e a terceira CategoriaFilmes apenas com chaves-estrangeiras para as duas primeiras. No entanto, esta não é a abordagem sugerida para o MongoDB. Aqui até podemos ter uma coleção de documentos Categorias, se necessária, mas a abordagem mais comum é usar um campo multivalorado no documento de filme contendo as categorias do mesmo. Simples assim.

Obviamente você deve se preocupar em garantir que as categorias sejam escritas sempre da mesma forma, a nível de aplicação, caso contrário será terrível filtrar por elas mais tarde. Enfim, nossa coleção de Filmes possuirá documentos com a seguinte estrutura:

Para subir o banco de dados do nosso microservice, apenas use uma instância do mongod apontando o dbpath para a pasta data dentro de cinema-microservice/movies-service/data. Obviamente em produção você terá uma abordagem diferente, mas ainda de um banco para cada microservice.

Conectando o banco

Agora que temos o modelo do nosso banco pronto e a estrutura de pastas organizada, vamos começar a programar nosso primeiro microservice.

Vamos começar acessando a pasta do nosso movie-service/src via terminal, criando um arquivo index.js na raiz desta pasta e usando o comando ‘npm init’ nela que é para criar o package.json do microservice. Depois, rode o comando abaixo pra garantir que teremos as nossas dependências mínimas garantidas.

Tem várias coisas que devemos fazer e não há necessariamente uma ordem certa para que elas funcionem. O primeiro microservice será um pouco chato e demorado de fazer, mas conforme a gente for avançando pelos demais você irá pegando o jeito. Sendo assim, vou começar por algo que acho que é mais fácil de todo mundo entender, o acesso a dados.

Dentro da pasta movie-service/src/config vamos criar um arquivo mongodb.js, com o seguinte conteúdo dentro:

Note que esse arquivo mongodb.js espera que existam duas variáveis de ambiente com a string de conexão ao banco. Essas variáveis de ambiente devem ser definidas em um arquivo sem nome com a extensão ‘.env’ na raiz do movie-service/src/, sendo que o pacote dotenv-safe que instalamos anteriormente exige a existência de um ‘.env.example’ com a definição das variáveis de ambiente existentes.

Para nos certificarmos que este módulo está funcionando, vamos escrever um teste unitário para ele? Se você nunca ouviu falar em testes unitários antes, recomendo ler este post sobre TDD.

Na mesma pasta movie-service/src/config crie um arquivo mongodb.test.js e dentro escreva o seguinte código, que nada mais faz do que usar a biblioteca tape (que foi instalada anteriormente no nosso npm install) pra testar a conexão:

Como teremos muitos arquivos de teste diferentes em nossa aplicação, cada um em sua pasta, vamos criar na raiz de movie-service/src um index.test.js que vai indexar todos nossos testes, a começar por esse primeiro, como abaixo:

Note que também carreguei o módulo do dotenv-safe pois precisamos que as variáveis de ambiente estejam carregadas para que nossos testes funcionem.

Falando em funcionar, antes de rodar este teste abra o seu packages.json que fica na raiz de movie-service/src e edite-o para que os scripts de start e de test fiquem igual abaixo:

Se você rodar agora sua aplicação com o comando abaixo, o seu unit test deve ser executado. Caso seu banco esteja online, obviamente.

MongoDB - Testes OK
MongoDB – Testes OK

Consultando o banco

Agora que sabemos que nossa conexão com o banco funciona, vamos criar nosso módulo de repositório para que possamos fornecer os dados do MongoDB da maneira que as chamadas ao nosso serviço esperam.

Não vou fazer um CRUD completo aqui pois já abordei CRUDs de Node com Mongo em outras oportunidades aqui no blog, é só procurar. Dentro do nosso case de exemplo levarei em conta que precisamos implementar apenas o R (Read) para fornecer dados de filmes específicos (por id) e dos filmes que são lançamentos nos cinemas (lançados nos últimos 30 dias).

Para criar nosso módulo de repositório (que por sua vez usará o módulo mongodb.js) entre na pasta movie-service/src/repository e crie dois arquivos, o repository.js e o repository.test.js, sendo que o primeiro deve ter o conteúdo abaixo:

Aqui temos uma função para cada um dos três métodos elementares que precisamos ter na APi e uma última para desconectar o repositório do banco de dados, função esta que será usada em certas ocasiões como em testes unitários.

E no segundo arquivo, repository.test.js, colocamos os testes do primeiro, de maneira análoga ao que fizemos com o módulo mongodb.test.js:

E por fim, adicione mais uma linha em nosso movie-service/src/index.test.js para que rode também este novo módulo de teste:

Obviamente que estes últimos testes não passarão se você rodar um ‘npm test’ no terminal, mas isso porque nosso banco de dados não possui qualquer informação de filme, o que você pode resolver abrindo uma instância do utilitário ‘mongo’ no terminal (executando um use no banco ‘movie-service’) e inserindo o comando abaixo para adicionar uma carga de filmes:

Agora sim, ao rodar o ‘npm test’, seus testes unitários devem passar com sucesso:

Repository Tests - OK
Repository Tests – OK

Com estes testes todos passando, temos a certeza de que a parte do banco de dados da nossa futura API estará 100% operacional, cabendo agora programarmos a API em si, que irá trabalhar com estes dados que viemos “brincando” até então.

No entanto, a programação da API movie-service ficou para a terceira parte desta série de artigos!

Curtiu o post? Então clica no banner abaixo e dá uma conferida no meu livro sobre programação web com Node.js!

Arquitetura de micro serviços em Node.js + MongoDB

Até o presente momento eu já escrevi alguns artigos aqui no blog sobre a arquitetura de micro serviços (microservices architecture), uma proposta de arquitetura antagônica aos tradicionais monolitos, ou grandes blocos de software que são o tipo mais comum de arquitetura presente atualmente nas empresas. Eu já estudava e gostava bastante deste modelo de arquitetura, que não é exatamente nova mas uma releitura de arquiteturas passadas, mas depois de passar a trabalhar no Agibank e ver os ganhos que temos com esta abordagem no dia-a-dia eu realmente fui convertido em um evangelista de micro serviços nas empresas. 🙂

Resumidamente um micro serviço é uma API, um backend que responde a uma única e exclusiva função, pautado no clássico SRP do SOLID (Single Responsibility Principle – Princípio da Responsabilidade Única). Cada micro serviço é independente dos outros e não possui qualquer tipo de dependência, tendo até mesmo a sua própria base de dados e servidor web em muitos casos, uma vez que esse é o único cenário real de 100% independência.

Obviamente sendo tão “micro” em sua responsabilidade, pouco se faz com apenas um microservice em um sistema inteiro, assim, é na chamada a múltiplos micro services que os comportamentos dos sistemas complexos são construídos, pautados em princípios como tolerância à falhas, containerização e escala horizontal. A Amazon é um símbolo desta proposta uma vez que alegam ter mais de 100 micro serviços sendo chamados durante a exibição de suas páginas de produtos no site Amazon.com. Outro ícone desta abordagem é a Netflix, que tem um serviço responsável por tentar derrubar seus micro serviços de maneira aleatória para testar sua resiliência, tolerância à falhas e interdependência entre eles.

E por fim, temos os testes e o deploy. É incrivelmente mais fácil ter uma cobertura de testes unitários automatizados que garantam o funcionamento de um sistema completo se eu tiver quebrado ele em micro-pedaços, assim como o deploy, que se torna bem menos oneroso e mais ágil neste contexto, podem fazer implantação de microservices de maneira independente desde que os contratos sejam mantidos intactos ou através de um sistema rigoroso de versionamento e APIs.

Ok, mas esta teoria eu já falei em outras oportunidades aqui no blog, certo? Mas como se cria um microservice na prática?

A ideia deste artigo é justamente é essa, mostrar como você cria um microservice profissional usando a plataforma do Node.js, uma das mais utilizadas neste tipo de abordagem (Netflix, LinkedIn, PayPal, etc). Em Java você poderia fazer algo semelhante também, usando Spring Boot, mas deixo isso para outra oportunidade.

Por que Node.js?

Lembra quando falei que a ideia é que o micro serviço seja independente do restante da aplicação? Pois é, uma das coisas que te garante independência é ter microservices rodando de maneira standalone, ou seja, em suas próprias plataformas.

Rodar microservices em Node.js permite que cada um deles tenha a sua própria instância de Node sem que isso pese muito no custo da empresa, uma vez que rodar Node é extremamente barato. Além de ser extremamente rápido de criar serviços standalone em Node, seja a partir do clássico exemplo de servidor em 12 linhas que está no site oficial ou usando web frameworks como ExpressJS (meu favorito).

Caso não esteja familiarizado com Node.js, ExpressJS e cia., recomendo ler os outros posts aqui do blog ou meu livro.

Por que MongoDB?

Idealmente cada serviço tem o seu próprio banco de dados, para que seja realmente independente dos demais. Sendo o domínio de aplicação extremamente enxuto, o banco de dados do microservice também o será. Uma coisa que eu acho extremamente positiva no uso de microservices com bancos independentes, principalmente em empresas que usam métodos ágeis, é que cada serviço pode ter a tecnologia de banco de dados mais adequado para sua função.

Assim, um microservice de catálogo de produtos pode usar MongoDB, enquanto que no mesmo sistema, o microservice de pagamento pode usar um SQL Server, enquanto que os sistemas estatísticos da empresa estão em um Cassandra. Óbvio que manter uma estrutura dessas em pé requer um time de engenharia forte, mas esse tipo de liberdade permite que os serviços sejam o mais eficiente possíveis e que os times que trabalham neles não fiquem presos a uma única suíte tecnológica, sendo autônomos e auto-organizados como o ágil prega.

Neste contexto, MongoDB é uma excelente opção na minha opinião, uma vez que sua natureza já prega a independência dos dos documentos mesmo entre as coleções do mesmo banco. Assim, conseguimos criar bancos poderosos, que escalam muito bem horizontalmente (um requisito muito comum em abordagens de micro serviços) e ao mesmo tempo simples, geralmente tendo uma coleção apenas.

Eu particularmente sou suspeito pra falar (afinal sou autor de livros sobre ambas plataformas), mas acho que Node e Mongo tem um fit absurdamente bom para compor soluções. Se você não sabe nada de MongoDB, sugiro ler os outros posts sobre o assunto aqui no blog ou o meu livro.

O que não abordarei?

Não falarei de DevOps aqui e consequentemente de deploy, CI, CD, etc. Quero me ater ao desenvolvimento do micro serviço sem ter de explicar Docker, API Gateway, etc.

Não falarei de front-end aqui. Ok, isso é meio óbvio, mas realmente não teremos interface gráfica neste tutorial. Primeiro porque não é meu intuito, segundo que não sou reconhecido pelos meus talentos com frameworks front-end. 😉

Não falarei dos princípios de desenvolvimento de web APIs RESTful, o que sugiro que você busque em outros materiais aqui do blog ou em meu livro de Programação Web com Node.js. Mas sim, usaremos estes princípios ao longo deste artigo.

Não falarei de versionamento ou documentação de APIs também (Swagger por exemplo).

Case de Exemplo: Cinema

Imagine que você está trabalhando no departamento de TI de uma grande rede de cinemas (ideia nem um pouco original, admito) que deseja refatorar todo o seu sistema de bilheteria e bomboniere de um gigantesco monolito para uma arquitetura de micro services. Como essa empreitada seria enorme para ser discutida apenas em um artigo de blog, vamos focar aqui no serviço de filmes, dentro da arquitetura abaixo.

Arquitetura de Micro serviços - Cinema
Arquitetura de Micro serviços – Cinema

Note que temos as responsabilidades já separadas neste desenho, dando um exemplo real de uma arquitetura usando micro serviços. Enquanto que em uma estrutura monolítica tradicional teríamos algo como um único webservice para o sistema de cinema inteiro (quem nunca fez um api.empresa.com na sua carreira?) ou no máximo dois, um para a bomboniére (Grocery Store) e outro para o sistema de ingressos em si (Booking).

Pensando em um dos casos de uso (ou histórias de usuário) mais comuns em um sistema de cinema temos o cliente que deseja ver os lançamentos no cinema mais próximo da sua casa. O diagrama abaixo ilustra um front-end mobile (app) consultando um serviço de cinemas que por sua vez consulta um serviço de filmes.

Consulta de Lançamentos do Cinema
Consulta de Lançamentos do Cinema

Por que dois serviços para entregar os lançamentos do cinema para o usuário?

Essa é a pergunta #1 de quem sempre construiu webservices monolíticos na sua vida.  A resposta é: segregação de responsabilidades (SRP, lembra?). O serviço de filmes (movies) tem apenas a preocupação de expor dados de filmes conforme consultado, sem qualquer regra de negócio ou julgamento quanto às chamadas. Já o serviço de catálogo do cinema (cinema catalog) é responsável pelas informações das salas de cinema existentes na rede e suas agendas.

Mas isso não é ineficiente, uma vez que temos de fazer sub-chamadas de serviços para atender a uma simples requisição do front-end?

Essa é a pergunta #2 de quem sempre buscou responder “desejos” do front-end em uma única chamada ao servidor. O engraçado é que eu recebo o mesmo tipo de indagação quando ensino programação orientada à objetos para quem está acostumado com procedural. Não são os milissegundos a mais que as sub-chamadas (seja entre serviços ou entre objetos) possuem que vão fazer a diferença na experiência de performance do sistema como um todo.

Como este artigo está ficando extenso, vou iniciar o desenvolvimento destes microservices na próxima parte desta série. Acesse a segunda parte aqui.

Curtiu o post? Então clica no banner abaixo e dá uma conferida no meu livro sobre programação web com Node.js!