Skip to content

026. Finalização do User

Filipe Deschamps edited this page Jan 18, 2022 · 1 revision

04/01/22

Muita coisa de legal aconteceu até a finalização do model User e a experiência de implementar ele sem usar ORM foi ótima, deu para se "conectar" bastante com o código e sentir o quão leve ele pode ficar (não precisar carregar na memória toda uma biblioteca grande pra fazer uma única query contra o banco de dados). Em paralelo, no quesito produtividade, eu diria que ganharia uma nota baixa, pois foi preciso mergulhar num mundo que já tinha sido abstraído completamente por uma solução como o Sequelize.

Mas acho que o mais importante de tudo não está relacionado ao Model em sí, mas em como boas práticas técnicas podem se tornar más práticas de projeto e atrapalhar o lançamento de um produto ou serviço. Isso me relembrou um certo padrão de "projetos desenvolvidos na coxa" mas que conseguem penetrar no mercado com mais facilidade por terem se dedicado primeiro a entender/descobrir o que as pessoas precisam. Mas a gente esbarra em outro problema que é não querer trabalhar num projeto que está malfeito, então o que faz ele nascer rapidamente, é o mesmo que faz ele morrer lentamente... é complicado, mas é uma discussão massa. No caso do TabNews, acho importante a parte técnica estar minimamente viável, porque um dos pontos dele é produzir conhecimentos e deixar eles registrados, como no caso desse diário e de vários outros materiais que vai dar para criar em cima disso.

Mas então voltando para a parte técnica, alguns conhecimentos interessantes desses últimos PRs #149 e #150:

Timezone no banco de dados

Uma pegadinha a ser evitada é salvar as datas no banco de dados sem nenhum timezone aplicado. Aí você deve estar se perguntando, "Como assim!? É sempre preciso salvar no tempo zero!" ... e ótimo, é isso aí, mas esse tempo deve ter consigo o timezone de "tempo zero", como por exemplo o UTC. Sem a informação de qual o fuso daquela data, como você vai saber como converter para outro fuso? Você pode assumir que estava em UTC, e é um ótimo palpite, mas ainda mais ótimo é você salvar a data com a informação de qual timezone ela possui, e que de preferência, deveria ser UTC. Fazer isso mantém seu dado mais consistente e evita um bug (ou feature) que sofri com o módulo pg. Ao detectar um campo de data, ele tem um parser que faz passar o valor por dentro de um new Date(valor) e caso esse valor não tenha o timezone consigo, faz retornar uma data no timezone local em que isso está sendo processado (que vai saber qual é).

Timeout

Eu sempre me esqueço o quanto timeouts são importantes pois, tirando o caminho feliz, quando você começa a traçar os caminhos tristes, geralmente se esquece de considerar que uma tentativa de conexão ao banco possa ficar presa para sempre (que foi o nosso caso) e que também deve ser incluida na lista dos caminhos tristes. Não ter mapeado que algo possa ficar preso para sempre pode lhe trazer comportamentos ruins para aplicação e uma ótima estratégia é se forçar a dar timeout em qualquer tipo de tentativa e preparar para sua aplicação reagir de forma adequada a isso. No TabNews há ainda vários espaços para adicionar timeouts.

Model e CRUD do User

Não é necessariamente um CRUD, porque não tem o Delete, mas as preocupações para esse Model e para a exposição de um endpoint seguem essa ordem (considerando que não foi implementado Autenticação nem Autorização):

  1. Uma Request chega e o método dela define qual Controller rodar.
  2. Como por exemplo, um POST irá cair no Controller de criação do User, onde será chamado o método create dele.
  3. Esse método create do User se responsabiliza por validar o Schema (as propriedades e valores enviados), pois assim ele poderá ser utilizado em outros controllers ou contextos, levando consigo essas validações.
  4. A validação está sendo feita com o módulo Joi, e ela automaticamente se responsabiliza pela coerção e formatação dos valores, como por exemplo, sempre garantir que um email seja salvo com todas as letras em minúsculo, ou com o trim() dos valores.
  5. Com essa validação/coercão finalizada, é verificado contra o banco a existência de um usuário com o mesmo username ou email.
  6. Se não conflitar, o usuário é criado e todos os dados são retornados pelo Model para o Controller.
  7. Na resposta final da request, o Controller filtra os dados que quer retornar (removendo dados como password) e retorna a request.

Custom Errors

O mais legal dessa implementação foi organizar como o fluxo entre o caminho feliz e triste foram controlados e em resumo é através dos Custom Errors. Todo Controller tem um catch global onde se nenhum erro customizado foi capturado/identificado, ele retorna um InternalError, que somente é outro erro interno. Então se você identificou um state que não pode continuar, como por exemplo uma propriedade do Model com valor inválido, basta dar um throw num ValidationError e isso é capturado nesse catch global e tratado de forma adequada, retornando um 400.

PUT vs PATCH

Outro detalhe interessante de se destacar foi o uso do PATCH ao invés do PUT para atualizar um objeto. Muita gente implementa o PUT com o comportamento do PATCH. Isso não é muito problemático, mas em algumas situações você entende o motivo por ter dois verbos separados, que é quando você quer deletar uma propriedade de um objeto, onde num PUT com comportamento de PATCH você precisa enviar as propriedades com valor null.

Testes automatizados

Isso deu bastante trabalho, mas acaba sendo a melhor parte pela segurança que os testes trazem ao fazer refatorações. Tudo foi implementado com testes de integração, pois eu queria garantir que o contrato da interface pública não seja quebrado em nenhum momento.

Fora isso, criei testes que garante comportamentos como as coerções que eu citei acima, como por exemplo um simples trim() e que poderia causar um dano muito grande de engenharia social. Por exemplo, se em alguma grande refatoração em que o Joi seja substituído para validação e o trim() deixa de ser feito sem a validação de alfanuméricos, vai ser possível criar dois usuários diferentes, o filipedeschamps e o filipedeschamps . Para evitar esse tipo de situação, foi criado um teste que bate contra a API criando esse usuário com um espaço adicional, e independente de como está implementado, o assertion vai querer garantir que o usuário a ser criado seja trimmed.

image