Desenvolvimento

30 mai, 2018

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

Publicidade

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

Na primeira parte desta série, eu dei uma introdução teórica sobre micro services e porque você deveria estar olhando para essa arquitetura, mas principalmente para as tecnologias Node e Mongo para implementá-la, Finalizei o 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 à 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

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:

const express = require('express');
const morgan = require('morgan');
const helmet = require('helmet');
var server = null;

function start(api, repository, callback){

    const app = express();
    app.use(morgan('dev'));
    app.use(helmet());
    app.use((err, req, res, next) => {
      callback(new Error('Something went wrong!, err:' + err), null);
      res.status(500).send('Something went wrong!');
    })

    api(app, repository);

    server = app.listen(parseInt(process.env.PORT), () => callback(null, server));
}

function stop(){
  if(server) server.close();
  return true;
}

module.exports = { start, stop }

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 construiremos 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 no meu site.

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:

const test = require('tape');
const server = require('./server');

function apiMock(app, repo){
    console.log("do nothing");
}

function runTests(){

    test('Server Start', (t) => {
        server.start(apiMock, null, (err, srv) => {
            t.assert(!err && srv, "Server started");
            t.end();
        });
    })
    
    test('Server Stop', (t) => {
        t.assert(server.stop(), "Server stopped");
        t.end();
    })
}

module.exports = { runTests }

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:

require("dotenv-safe").load();
require("./config/mongodb.test").runTests();
require("./repository/repository.test").runTests();
require("./server/server.test").runTests();

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

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:

module.exports = (app, repository) => {

  app.get('/movies', (req, res, next) => {
    repository.getAllMovies((err, movies) => {
      if(err) return next(err);
      res.json(movies);
    });
  })

  app.get('/movies/premieres', (req, res, next) => {
    repository.getMoviePremiers((err, movies) => {
      if(err) return next(err);
      res.json(movies)
    });
  })

  app.get('/movies/:id', (req, res, next) => {
    repository.getMovieById(req.params.id, (err, movie) => {
      if(err) return next(err);
      res.json(movie)
    });
  })
}

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:

const test = require('tape');
const supertest = require('supertest');
const movies = require('./movies');
const server = require("../server/server");
const repository = require("../repository/repository");

function runTests(){

    var app = null;
    server.start(movies, repository, (err, app) => { 
        var id = null;
        test('GET /movies', (t) => {
            supertest(app)
                .get('/movies')
                .expect('Content-Type', /json/)
                .expect(200)
                .end((err, res) =>{
                    if(res.body && res.body.length > 0) id = res.body[0]._id;
                    t.error(err, 'No errors')
                    t.assert(res.body && res.body.length > 0, "All Movies returned")
                    t.end()  
                })
        })
        
        test('GET /movies/:id', (t) => {
            if(!id) {
                t.assert(false, "Movie by Id Returned");
                t.end();
                return;
            }

            supertest(app)
                .get('/movies/' + id)
                .expect('Content-Type', /json/)
                .expect(200)
                .end((err, res) =>{
                    t.error(err, 'No errors')
                    t.assert(res.body, "Movies By Id returned")
                    t.end()  
                })
        })

        test('GET /movies/premieres', (t) => {
            supertest(app)
                .get('/movies/premieres')
                .expect('Content-Type', /json/)
                .expect(200)
                .end((err, res) =>{
                    t.error(err, 'No errors')
                    t.assert(res.body && res.body.length > 0, "Premiere Movies returned")
                    t.end()  
                })
        })

        server.stop();
     })
}

module.exports = { runTests }

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 artigo 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óprios olhos.

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 micro serviç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:

require("dotenv-safe").load();
const movies = require('./api/movies');
const server = require("./server/server");
const repository = require("./repository/repository");

server.start(movies, repository, (err, app) => { 
    console.log("just started");
});

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

Algumas boas práticas relacionadas a construção de APIs RESTful podem ser encontradas neste ótimo artigo (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 eu 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 artigo da série!