Guia de desenvolvimento - Backend
Este documento é leitura obrigatória antes de iniciar o desenvolvimento do backend. Ele contém informações importantes sobre arquitetura, documentação e organização física do projeto.
Índice
Organização física
As principais estruturas do projeto são:
-
README.md
: informações sobre o projeto, instruções de configuração do ambiente e documentação dos scripts de execução e manutenção -
package.json
epackage-lock.json
: gerenciamento de dependências com NPM -
.[babel,eslint,husky,jest,prettier]rc.js
,.eslintignore
enodemon.json
: configurações de pacotes auxiliares (transpiler, linter, formatter, git hooks, hot reload, testes, ...) -
.env
: variáveis de ambiente para execução do servidor -
docs/
: documentação-
update.js
: script para atualizar a documentação local -
dTool API.postman_collection_.json
: collection do Postman com requests da API -
DT.postman_environment.json
: environment do Postman com placeholders para as variáveis -
DT-development.postman_environment.json
: environment do Postman com valores padrão para desenvolvimento
-
-
dist/
: código transpilado (que é executado pelo NodeJS) -
src/
Documentação
Todas as rotas da aplicação serão documentadas usando o Postman. O Postman é uma aplicação que permite a fácil execução de requests HTTP e análise dos retornos, ajudando a testar uma rota durante seu desenvolvimento (se o retorno da rota está de acordo com o especificado na documentação).
Você pode consultar a documentação das rotas a serem desenvolvidas pelo navegador e importar a definição das rotas para o aplicativo usando o botão Run in Postman. Junto a cada rota, serão anexados exemplos de entrada e retorno esperado, servindo como referência para o desenvolvimento tanto do backend quanto do app.
Arquitetura
O backend está organizado em uma arquitetura com três camadas:
- controllers (pasta
src/routes
): trata os parâmetros das rotas, utiliza serviços para executar as ações necessárias para o correto funcionamento da rota e controem o retorno da rota (código de status, formato, ...); - services (pasta
src/services
): camada de negócio, recebe os parâmetros já parseados pelas controllers e usa os DAOs para acesso ao banco de dados; - models (pasta
src/models
): camada de persistência, provê acesso ao banco de dados.
Seguindo o padrão arquitetural em camadas, é importante que objetos de uma camada troquem mensagens apenas com objetos da camada imediatamente inferior, ou objetos da mesma camada. Por exemplo: controllers podem acessar/usar apenas services; services podem usar outros services ou modelos/DAOs (mas não podem acessar controllers).
Controllers
A camada de controllers (controladoras) é responsável por definir o tratamento para cada rota da API. Elas usam diretamente a API exposta pelo framework Express, tratando os parâmetros de cada rota (sejam parâmetros recebidos no body do request, como parte do caminho da rota ou como query), convertendo-os para um formato/estrutura aceito pelo(s) serviço(s) usados, fazendo invocações aos serviços e construindo as respostas de acordo com o retorno dos serviços.
Funcionamento básico
- tratar parâmetros usando API do Express;
- ajustar parâmetros para formato/estrutura esperada pelo(s) serviço(s) usado(s);
- executar a(s) chamada(s) ao(s) serviço(s);
- construir a resposta do request (código de status e payload) a partir da resposta do(s) serviço(s).
Organização física
Todas as controllers estão na pasta src/routes
, e devem ser organizados de acordo com a rota tratada. Por exemplo:
- o arquivo
src/routes/technology.js
trata os requests de todos os verbos aplicáveis (GET
,POST
, ...) à rota (e subrotas)/api/technology
:GET /api/technology/3
,POST /api/tecnology
, ... - da mesma forma, o arquivo
src/routes/healthInstitution.js
cuida da rota/api/healthInstitution
.
O arquivo src/routes/index.js
exporta uma função que faz a configuração de todas as rotas da API. Essa função é invocada por um loader como parte do processo de inicialização do servidor.
Criando novas controllers
Sempre que for preciso criar o controller para uma nova rota:
- o arquivo do controller deve exportar uma função que recebe um Router
- a função exportada pode ser
async
(export default async (appRouter) => { ... }
)
- a função exportada pode ser
- a controller deve ser adicionada ao processo de configuração no arquivo
src/routes/index.js
:- importar a controller:
import controller from "./controller"
- invocar a função da nova controller:
controller(appRouter)
- importar a controller:
Implementando controllers
Os serviços que podem ser usados pelas controllers estão na pasta src/services
, e devem ser importádos de lá.
Importante
Para manter a padronização, um serviço não deve ser importado a partir do arquivo onde está definido, mas apenas do arquivo de onde todos os serviços são exportados:
❌ import AlphaService from "../services/AlphaService"
✅ import { AlphaService } from "../services"
O acesso a serviços é feito com o uso de um service locator: um objeto central que instancia e provê os serviços para uso pelas controllers. Para usar um serviço, é preciso:
- importar o service locator:
import { Container } from "typedi"
(estamos usando o pacote typedi) - importar a classe do serviço:
import { AlphaService } from "../services"
- obter uma referência ao serviço:
const alphaService = Container.get(AlphaService)
Services
A camada de serviços é responsável pela lógica de negócio da aplicação. Ela faz as validações necessárias sobre os parâmetros, "joga" (throw
) erros para tratamento pelas controllers, e faz modificações no banco de dados quando necessário. Um serviço também pode usar outro(s) serviço(s) para seu funcionamento.
Geralmente, um serviço é responsável pelas regras de negócio relacionadas a uma entidade de negócio. Por exemplo: HealthInstitutionService
agrupa regras relativas às instituições de saúde; TechnologyService
, relativos às tecnologias (procedimentos); e assim por diante.
Nota
Ainda não há uma definição exata quanto à granularidade e limite de responsabilidades dos serviços. Isso será discutido ao longo do projeto, caso a caso.
Organização física
Todos os serviços estão na pasta src/services
. Serviços que podem ser usados por controllers devem ser exportados no arquivo src/services/index.js
.
Criando novos serviços
Sempre que for necessário criar um novo serviço:
- criar um arquivo com o nome da classe do serviço:
src/services/BetaService.js
- criar e exportar a classe do serviço:
export default class BetaService { ... }
- re-exportar o novo serviço no arquivo
src/services/index.js
:export { default as BetaService } from "./BetaService"
Implementando serviços
Não é possível definir uma regra geral de formato de implementação dos serviços: depende do objetivo do método e entidade relacionada. Entretanto, alguns métodos CRUD de serviços possuem uma implementação parecida:
- validação das entradas
- invocação de alguma operação da camada de modelos (create, recover, update, delete, ...)
- filtro sobre o retorno da camada de serviço para retorno à controller
Ressaltando: não há um esqueleto de como é a implementação de um método de serviço.
Models (DAOs)
A camada de modelos encapsula a persistência dos dados e comunicação com o banco de dados. Os objetos dessa camada também são chamados de DAO (Data Access Object), por conta do padrão de projeto que implementam.
Como o backend comunica-se com o SGBD PostgreSQL, essa camada é desenvolvida com o pacote Sequelize, que faz o processo de mapeamento entre os modelos relacional e orientado a objetos (processo conhecido como ORM).
O uso do Sequelize como ORM permite acelerar o desenvolvimento ao não ter que implementar métodos CRUD simples que o Sequelize já provê, com create, list, update, entre outros.
Essa camada está intimamente ligada ao modelo físico de armazenamento dos dados no PostgreSQL: a definição de um modelo depende das tabelas, seus campos, chave primária e chaves estrangeiras (relacionamentos).
Organização física
Todos os modelos devem ficar na pasta src/models
. O nome do arquivo de definição de cada modelo deve ser composto pelo nome do modelo (em UpperCammelCase), seguido de DAO
. Cada arquivo deve definir apenas um modelo.
Criando novos modelos
Supondo que seja preciso criar um modelo para representar uma pessoa:
- criar um arquivo
src/models/PersonDAO.js
- adicionar os imports necessários do Sequelize:
import { Model, DataTypes } from "sequelize"
- exportar uma classe default que herda da classe
Model
do Sequelize:export default class PersonDAO extends Model { ... }
(geralmente não há métodos customizados na classe; mais na próxima seção) - exportar uma função, de nome
setup
, que recebe como parâmetro o objeto do Sequelize para definição do modelo:export const setup = (sequelize) => { ... }
- definir o modelo dentro da função
setup
usando a API do Sequelize.
Implementando modelos
Geralmente, não é preciso implementar operações customizadas na maioria dos modelos, apenas as operações providas pelo Sequelize são suficientes (create, list, delete, ...).
Entretanto, se necessário, é possível criar métodos customizados na classe do modelo:
export default class PersonDAO extends Model {
static performComplexQuery(parameters) {
...
}
}