Se você usa Node, uma das primeiras coisas que deve ter aprendido é como criar sua API Rest usando Express. O titulo é meio chamativo e forma que veremos Express daqui pra frente é para mudar um pouco nosso padrão de construção de APIs, mas é comentado por alguns, mas não é ensinado a não ser que você se aprofunde sobre o assunto.

Vamos falar sobre arquitetura

Para montarmos uma base, arquitetura em software é a parte de estrutural dele [1], e poderiamos dizer que o padrão de arquitetura mais conhecido é o MVC, nesse padrão separamos o nosso software em camadas que chamamos de Model, View e Controller, poderíamos chamar em português de Modelo, Visual e Controlador?

Quando falamos de MVC no Express, geralmente separamos nosso lógica dentro de pastas controladores, schemas e comunicação com o banco de dados dentro dos modelos. E parte visual deixamos de lado porque já não se encaixa tanto no modelo de APIs REST.

E isso acontece com outras arquiteturas menos conhecidas deixando a impressão de que sempre estamos fazendo uma gambiarra no framework mais usado em NodeJS. Por exemplo a arquitetura de tres camadas, também não parece funcionar tão bem.

Mas porque isso parece não funcionar?

Precisamos falar também sobre Design

Uma coisa que acho interessante sobre desenvolvimento de software, é o tanto de analogias de outras areas de conhecimentos que usamos, mas não olhamos para alguns conceitos dessas outras areas.

‌‌‌‌Forma segue a função, de forma simples, é um principio do design modernista em que, troca o uso de materiais caros e inacessíveis usado para embelezar produtos, deixando produtos cada vez mais caros, para produtos que seguissem sua principalmente sua função e proposito sem perder a beleza. [2]

Isso também pode ser extendido para o conceitos de engenharia de software como o DDD e também a busca através do TDD, mas ao falarmos de ferramentas, elas também tem formatos que seguem uma função especifica.

Vamos falar sobre a função principal do Node. Fazer input e output da forma correta, Isso é realmente importante, já que era o problema que o Ryan Dahl gostaria de resolver era o consumo e latencia de inputs e outputs. [3].

A forma em que o Express trabalha respeita e segue a função do Node e ao criar nosso software também dessa forma, evitamos de usar a ferramenta errada para resolver problemas e terminando com mais problemas do que soluções.

Já que falamos de design, vamos falar um pouco sobre arquitetura do Express e como podemos estruturar melhor nossas aplicações.

Funcionalidade do Express

A principal funcionalidade do Express é ser uma web framework, uma requisição web tem seus passos entre uma requisição e resposta. A ideia principal do Express é ser simples e muito próximo do padrão Unix, fazer uma só coisa e fazer bem. Tanto que tivemos alguns mudanças drasticas da versão 3 para 4, nesse sentido. ‌‌‌‌Já que temos que em uma requisição HTTP, executamos alguns passos, vamos analizar alguns deles. Primeiro em uma requisição simples, temos a seguinte lista de passos:

  • Validações

- de autenticação

- de dados

  • Processamento

- Chamada de um serviço externo para dados adicionais

- Modificação e enriquecimento de algum dado

  • Persistencia

- Salva dados no banco

- Transmitir dados para um serviço externo

  • Resposta

- Construção do dado de resposta

Se fossemos dividir uma requisição HTTP poderiamos dizer que temos essas ações e não queremos que elas sejam bloqueantes para thread.

Nos padrões de arquitetura que vimos anteriormente temos essas sequencias divididas em controladores, modelos e camadas de negócio, novamente não parece fazer tanto sentido assim. Já que vemos claramente uma lista de ações a serem executadas.

Pipes and filters

Pipeline que podemos traduzir por condutores, é algo já bem conhecido em programação, principalmente quando usamos sistemas baseados em Unix , sempre faz sentido quando temos que executar ações sequencias como descrevemos anteriormente.

Resumidamente a arquitetura de pipes and filters, trata o dado como um fluxo dentro de um condutor e você pode adicionar os filtros para tal dado, seja direcionando o fluxo prosseguindo com ele.

Esse padrão conversa muito bem com o Express para ganho de performance e na forma de execução. Na verdade já utilizamos ele um pouco dessa forma, a cada middleware que adicionamos à função .use é uma tarefa em um pipeline de execução.

Mas podemos ir um além e devemos ir além, para construir nosso web server não bloqueante. A ideia é sempre essa, evitar tarefas custosas que podem travar o event loop. NÃO TRAVE O EVENT LOOP. [4]

Vamos ao código?

Geralmente nesse fluxo de pipes e filters o primeiro que criamos é o CORS. Usamos para das as permissões para requisições post devolvendo a requisição do tipo OPTIONS com status 200, permitindo acesso de outras URLs.

Como exemplo inicial vamos construir um pipe de criação de usuário e salvar ele em um banco ocutando algumas informações.

Como dito o primeiro filtro que vamos criar é o do CORS, sei que já existe uma biblioteca para fazer isso, mas vamos criar assim para facilitar o entendimento e também usar o body parser.

Para organizar o código vamos criar uma pasta de task, em que cada task é um filtro na pipeline. A nossa estrutura de pastas para exemplo é a seguinte:

ExpressNGMEnsina
  index.js
  app.js
  /schemas
  /tasks
    cors.js
Estrutura de pastas esperadas, essa não deve ser a estrutura em um projeto real, está assim para fins de clareza da ideia

E o nosso código:

module.exports = (request, response, nextTask) => {
  if (request.method !== 'OPTIONS') return nextTask();
  response.header('Access-Control-Allow-Origin', '*'); 
  response.header('Access-Control-Allow-Methods', 'GET, PUT, PATCH, POST, DELETE');
  response.header('Access-Control-Allow-Headers', 'Origin, X-Requested-With, Content-Type, Accept, Authorization');
  return response.status(200).send();
};
Código que deve estar no arquivo cors.js dentro da pasta tasks

É importante observar na segunda linha que já determinamos o que filtramos, qualquer requisição OPTIONS, será executada aqui.‌‌‌‌Nosso app.js fica da seguinte forma:

//Os imports foram ocultados, mas estarão disponiveis no github

const app = express(); 
app.use(CORS);
Código dentro do index.js

Assim já definimos um filtro para todos as requisições o mesmo pode ser feito usando o body parser.

Mas o que dizer de algo especifico das rotas?

A sistema de roteamento do Express também permite ações através dos middlewares. Vamos destrinchar os itens da nossa criação do usuário, sendo a primeira coisa que faremos é validar o payload de criação do usuário.

Para facilitar isso, vamos criar um routes.js na raiz do projeto e usar o Router() do próprio Express e nossa rota será um POST do user.

  1. Validações

Como já listado, precisamos validar as permissões do usuário e também dos dados enviados por ele.

Para a autenticação, validaremos o token pré-criado para escrita e vamos precisar criar nossa primeira task de validação:

module.exports = (request, response, nextTask) => {
  const { authorization } = request.headers;
  
  jwt.verify(authorization, '666', err => {
    if (!err) return nextTask();
    
    return response.status(403).send('unauthorized');
  });
 };
Código da tasks de authorization

Observe que assim como no arquivo CORS, assim que o usuário se torna válido ele direcionado para o próximo passo do nosso pipeline que é validação dos dados.

Os dados ele devem seguir modelo abaixo.

// Para facilitar na validação dos dados vamos usar o JOI
module.exports = Joi.object({
  name: Joi.string().required(),
  github: Joi.string().required(),
  password: Joi.string().required(),
});
Schema do objeto user

Usaremos esse schema como parte do nosso filtro e com isso vamos criar mais uma task de filtro de dados, que chamaremos de validation.

module.exports = schema => async (request, response, next) => {
  try {
    request.validaData = await schema.validateAsync(request.body);
    return next();
  } catch(e) {
    return response.status(400).send(e);
  }
};
Código da task de validação dos dados

Para validarmos os dados usamos Joi do Hapi, novamente o dado sendo válido segue para a próxima etapa ou saí do fluxo.

Agora é a etapa de persistencia da nossa API, mas poderíamos ter a etapa de consulta para um outro serviço. O código da nossa persistencia é o seguinte.

module.exports = userModel => (request, response, next) => {
  userModel(request.validData, err => {
    if (!err) return next();

    return response.status(503).send(err);
  });
};
Tarefa de persistencia

Você deve ter notado que criamos uma função UserModel, ela abstrai a query do banco e código é bem simples.

'use strict';

const sqlite3 = require('sqlite3').verbose();
const db = new sqlite3.Database('./exdb.db');

module.exports = ({ name, github, password }, cb) => 
  db.serialize(() => db
    .prepare(`INSERT INTO users VALUES (?, ?, ? )`)
    .run(name, github, password)
    .finalize(cb));
Modelo de criação do usuário no banco de dados usando SQLite

Tão simples quanto isso e vamos para etapa de resposta da nossa API, que será bem simples também.

module.exports = (request, response) => {
  const { name, github } = request.validData;

  response.status(201).send({
    status: 'created',
    name,
    github
  });
};
Após todas as etapas validadas a nossa resposta

Resumindo

O Express fica com códigos muito mais fácil, legivel e testável, após estruturar o projeto usando esse conceito. Aumenta o reuso de partes do software, por exemplo a etapa de validação pode ser a sempre a mesma ou até a persistencia, nada impede de usarmos esse pipeline de post para um get. Com excessão do nosso arquivo de resposta, que pode criar uma abstração para ele.

Ainda podemos usar outros conceitos para levar essa API. para um outro nível como injeção de dependencias e também algo mais próximo da arquitetura limpa.

Testes unitários de uma função com uma única responsabilidade também fica mais fácil e simples, o que demonstra o caminho correto do desenvolvimento.

Dúvidas, sugestões e correções só chamar. :)


Um agradecimento especial para o Marcus Ortense por ter comentado isso em uma das excelentes conversas sobre engenharia de software.

UPDATE: Esse é um agradecimento adicional para o Thiago Arrais, que notou uns erros nos exemplos de códigos e também no statuscode da etapa final. Valeu!

[1] https://en.wikipedia.org/wiki/Software_architecture

[2] https://en.wikipedia.org/wiki/Form_follows_function

[3] https://www.youtube.com/watch?time_continue=2&v=ztspvPYybIY&feature=emb_title

[4] https://nodejs.org/en/docs/guides/dont-block-the-event-loop/

https://docs.microsoft.com/pt-br/azure/architecture/patterns/pipes-and-filters