- Express
- MariaDB
- Typescript
- TypeORM
- Teste unitários com JEST
- Github Actions
Esse repositório tem o objetivo de auxiliar no ponto de partida da criação de uma API Node. A API foi desenvolvida visando boas práticas mas melhorias são sempre bem-vindas, caso achar que algo possa ser feito melhor, abra uma PR que irei avaliar.
A API segue a lógica da imagem abaixo:
Abaixo será descrita a estrutura de pastas do código da API
Todo o código da API se encontra na pasta ./src
:
Tipagens adicionais, como por exemplo, foi incluído um objeto user com a propriedade id no objeto de request do express para ser utilizado no middleware de autenticação;
Arquivos auxiliares de configuração como configuração de base de dados, token JWT, e etc.
Os módulos da API, como ponto de partida foi criado o módulo de usuários que possui a estrutura necessária para a criação de outros módulos que a API venha a suportar. A escolha por criar um módulo de usuários é que esse é o módulo mais comum a ser criado e normalmente o primeiro.
São as entidades, ou seja, são as classes que definem como os dados serão trabalhados dentro do módulo e como as tabelas do banco de dados serão criadas.
Contém as rotas pertinentes ao módulo, nesse módulo foram criadas as rotas de usuários e de autenticação.
São as classes chamadas pelas rotas. Como boa prática um Controller deve possuir no máximo 5 métodos:
- store: Criação de entidades
- update: Atualização de entidades
- destroy: Deleção de entidades
- show: Pesquisar uma entidade singular
- index: Pesquisar mais de uma entidade
Cada um desses métodos deve utilizar como parâmetros request
, response
e retornar uma Promise<response>
Dentro dos controllers não ocorrem validações, apenas o recebimento de dados do request, instanciação e execução dos Services
e retorno do response
.
São as classes principais da API, são neles que as regras de negócio da API são validadas. Um Service
deve possuir apenas um método público chamado execute
, ou seja, um Service
só pode possuir uma responsabilidade, de executar o serviço pelo qual foi criado.
O construtor de um Service
deve receber as dependências que serão utilizadas dentro dele como por exemplo os repositórios de entidades que irá utilizar para realizar o seu serviço, além de provedores de outros serviços específicos como um provedor de hashs para criptografia de senhas, etc.
As dependências de um Service
são injetadas no mesmo utilizando a lib tsyringe
fazendo com que fique muito simples mudar a dependência que ele utiliza e realizar testes unitários.
É importante que a interface de dados que o método execute
receber seja o mesmo tipo que será passado para o repositório principal desse Service
, exemplo: No CreateUserService
, foi utilizada a interface ICreateUserDTO
como tipagem dos dados do método execute
e essa é a mesma interface do método save
do UserRepositories
.
As requisições dos clientes são recebidas aqui, todas as rotas da aplicação são importadas no arquivo ./src/shared/routes/index.ts
Dentro da pasta services de cada módulo há uma pasta contendo os arquivos de teste. Cada Service possui um arquivo de teste respectivo, sugiro sempre que for criar um novo service, iniciar criando-o e criar o teste respectivo antes de criar o Controller, rotas e etc. Seguindo os conceitos de TDD.
Para rodar todos os testes basta rodar o comando yarn test
. Será gerado um arquivo html
com o relatório dos testes dentro de ./coverage/lcov-report/index.html
.
São abstrações do banco de dados, nessa API foi utilizado o typeORM para criar essas abstrações, mas pode ser feito com outro ORM tranquilamente.
Além do repositório do banco de dados, também são criados repositórios idênticos chamados de fakes. Esses fakeRepositories são utilizados como mock nos testes unitários dos Services
e devem implementar as mesmas interfaces que o repositório do BD implementa. Essas interfaces estão localizadas dentro de ./src/modules/**/repositories/dto/I<entity>Repositoriy
Contém arquivos que são compartilhados entre módulos
Aqui é onde as dependências que serão injetadas nos Services
estão sendo definidas, note que nem todas as dependências estão nesse arquivo, pois algumas estão registradas na pasta providers e apenas é importado o arquivo index.ts de providers, dessa forma fica mais organizado.
O arquivo index.ts
da pasta container deve ser importado no topo do arquivo server.ts
que é o arquivo rodado inicialmente ao subir o server.
Arquivos contendo as Exceptions
, note que não há um try-catch nos controllers
, portanto, os throws
que são feitos dentro dos Services
são pegos pelo middleware
de erros localizado dentro dessa pasta e importado dentro de server.ts
.
Dentro dessa pasta também está um ENUM
de códigos HTTP que pode ser útil.
Sempre que criar uma nova Exception
que receber um statusCode
, tipar esse code com o type HTTPStatusType
exportado dentro do arquivo HTTPStatusEnum.ts
, vide exemplo das outras Exceptions
.
São implementações de dependências externas, essas implementações devem respeitar uma interface definida dentro da pasta do provider, essa interface deve ser implementada também no provider Fake para que seja possível utilizá-lo nos testes unitários e manter os testes sem dependências externas.
Local onde os arquivos de migrations são armazenados, comandos úteis:
- Criar nova migration:
yarn typeorm migration:create -n <nomeDaMigration>
- Rodar as migrations pendentes:
yarn mig:run
- Reverter a última migration:
yarn mig:revert
OBS: Os scripts mig:run
e mig:revert
rodam antes o build com o babel, isso é necessário pois as migrations são rodadas na pasta dist, dessa forma, facilita o deploy futuramente.
Algumas funções úteis para serem utilizadas, num primeiro momento criei apenas funções para utilizar nos testes unitários, foram usados tipos genéricos, portanto, não importa a entidade que o repositório possua, essa funções irão funcionar, mas qualquer bug me avise :D
Após criar o repositório rode yarn
para instalar todas as dependências.
É necessário ter o node instalado, tenha a última versão LTS
# Criação do conteiner
docker run --name mariadb -e MYSQL_ROOT_PASSWORD=<root-pass> -p 1234:3306 --restart always -d mariadb
# A tag "--restart always" reinicia o container automaticamente caso ele cair.
# Acessar o conteiner
docker exec -it mariadb /bin/bash
# Atualizar os pacotes
apt-get update
# Instalar o sudo
apt-get install sudo
# Acessar o banco com o usuário root
sudo mysql -u root -p
# Criar o banco de dados
CREATE DATABASE <database>;
# Criar o usuário que será utilizado pela aplicação (nunca utilizar o usuário root)
CREATE USER <user>@localhost IDENTIFIED BY '<senha>';
# Aplicando as permissões para o usuário criado no branco de dados
GRANT ALL PRIVILEGES ON <database>.* TO <user>@localhost IDENTIFIED BY '<senha>';
FLUSH PRIVILEGES;
# Ajustar timezone para Brasil
SET @@global.time_zone = '-3:00';
QUIT
Para iniciar o container: docker run mariadb
Após criar a base de dados e iniciá-la, crie o arquivo .env
a partir do arquivo .env.example
e preencha os dados referente ao banco de dados.
Comando: yarn mig:run
-> Irá rodar o build e as migrations pendentes para a criação das tabelas no BD
Agora que o BD está criado e as dependências instaladas, rode yarn dev:server
para rodar a API, o console irá mostrar essa mensagem:
✅ - back-end rodando! na porta 3333
✅ - Conectado ao DB
Abaixo segue um exemplo de workflow para fazer o deploy na Digital Ocean
name: CI/CD Digital Ocean
on:
push:
branches: [ main ]
jobs:
build:
name: CI Pipeline
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v2
# This caches all of your node_modules folders throughout your repository,
# and busts the cache every time a yarn.lock file changes.
- uses: actions/cache@v2
with:
path: '**/node_modules'
key: ${{ runner.os }}-modules-${{ hashFiles('**/yarn.lock') }}
- name: Setup Node.js environment
uses: actions/setup-node@v2.1.4
with:
node-version: 14.x
# Instalar dependências
- name: Install Dependencies
run: yarn
# Executar build
- name: Run build
run: yarn build
# Executar build
- name: Run tests
run: yarn jest --coverage
deploy:
name: CD Pipeline
runs-on: ubuntu-latest
needs: build
steps:
- uses: actions/checkout@v2
# Copiar todas as pastas para a Digital Ocean
- name: Copy files to Digital Ocean Server, except node_modules
uses: appleboy/scp-action@master
with:
host: ${{ secrets.SSH_HOST }}
username: ${{ secrets.SSH_USER }}
port: ${{ secrets.SSH_PORT }}
key: ${{ secrets.SSH_KEY }}
source: ".,!node_modules"
target: "~/app/bossabox-api-desafio/"
# Dependências, testes, migrations, reiniciar servidor
- name: Dependencies, migrations, server restart
uses: appleboy/ssh-action@master
with:
host: ${{ secrets.SSH_HOST }}
username: ${{ secrets.SSH_USER }}
port: ${{ secrets.SSH_PORT }}
key: ${{ secrets.SSH_KEY }}
script: |
cd ~/app/bossabox-api-desafio/
yarn
yarn build
yarn typeorm migration:run
pm2 restart bossabox-api