Recentemente, Oren Eini (Ayende) publicou um post analisando o código de parte de uma API que continha uma falha grave de design. Nesse post, reproduzimos algumas das ideias que ele compartilhou e adicionamos algumas considerações que consideramos relevantes.
[Route("/public/api/v1/tickets/{org}")] public async Task<IActionResult> Get(string org, int skip = 0) { var tickets = await session.Query<Domain.SupportTicket>() .Where(x=>x.Organization == org) .OrderByDescending(x => x.LastUpdate) .Skip(skip) .ToListAsync(); return Ok(tickets); }
O problema de design apontado por Oren é a exposição de um objeto de domínio em uma API pública (feita para ser consumida em aplicações desenvolvidas por outros times, fora da empresa). Essa, aliás, é uma dívida técnica extremamente comum contraída, geralmente, por inocência ou falta de conhecimento.
Um dos maiores problemas de expor um objeto de domínio em uma API pública é o acoplamento. Como Oren destaca, tal decisão vincula, para começar, o versionamento do domínio ao versionamento da API e, eventualmente, mudanças no domínio com potencial para tornar código consumidor incompatível podem “passar” de forma inadvertida.
Outro problema a considerar é o potencial vazamento de dados sensíveis. Afinal, é normal que objetos de domínio sejam “enriquecidos” na medida que o projeto avança.
Veja também
- LGPD – Lei Geral de Proteção de Dados – nº 13.709/2018
- Cuidado! Sua empresa pode ser solidariamente na LGPD
Um problema adicional, em nossa análise, é que o acoplamento resultante da exposição de objetos de domínio direciona o desenvolvimento para monolíticos distribuídos.
A recomendação de Oren foi criar um modelo exclusivamente para compartilhamento da API. Além disso, ele recomendou a não utilização de bibliotecas de mapeamento como o Automapper. Segundo ele, é importante evitar qualquer chance de exposição acidental.
[Route("/public/api/v1/tickets/{org}")] public async Task<IActionResult> Get(string org, int skip = 0) { var tickets = await session.Query<Domain.SupportTicket>() .Where(x=>x.Organization == org) .OrderByDescending(x => x.LastUpdate) .Skip(skip) .Select(ticket => new PublicTicketDto { Subject = ticket.Subject, LastUpdate = ticket.LastUpdate, Status = ticket.Status, // etc }) .ToListAsync(); return Ok(tickets); }
Na EximiaCo, defendemos a categorização de APIs como internas ou externas. Nossa experiência é que APIs de propósito geral costumam gerar baixa adesão, além de outros prejuízos (como aumento de consumo da rede e requisições desnecessárias).
Veja também
A categorização de APIs como propomos é um exemplo de como decisões arquiteturais mitigam os riscos de decisões ruins de design como a que estamos tratando nesse post.
Dissemos, no passado, que nem tudo que parece divida técnica, efetivamente, é. Entretanto, nesse caso, temos um problema oposto – uma dívida técnica que é difícil de ser percebida como tal.
Os “juros” dessa dívida técnica podem levar muito tempo para serem percebidos. Mas, quando finalmente a “cobrança” acontece, podem ter impactos difíceis de absorver.
Independente de ser publica a API, considero uma boa prática a exposição por meio de DTOs em casos simples (com CQRS pode ser query result).
É uma ótima abordagem, mas e quando estamos falando de um Enum ou de um objeto de valor voltado deste domínio?
Imagine que temos um enum TicketStatus, esse DTO poderia expor esse enum? E quanto a um objeto de valor?
Falando sobre os auto mappers. Um erro comum que eu vejo é na mistura auto mapper + EF + Lazy Collections. Várias e várias vezes fui analisar problemas de performance e encontrei coleções desnecessárias sendo entregues, e o culpado 90% das vezes é mapeamento inocente das coisas.
[]s
Esta abordagem é bem interessante e assunto de extrema importância, dado que versionamento de API acaba se tornando doloroso e gera problemas com integrações, outros times, quando fazemos a exposição de entidades de domínio diretamente.
No caso para exposição e complementando a pergunta do Tiago Santos, Enums e outros objetos complexos, devemos expor eles de forma mais primitiva possível (string, int, etc) do que com Enums ou devemos refletir estes tipos mais “complexos”?
Excelente parabéns e obrigado!
Gustavo, particularmente gosto mais da expressividade, logo, retornaria uma string com que o enum significa.
O XML tem uma vantagem que não sei se existe nas documentações das APIs REST que é uma espécie de dicionário dos valores possiveis sobre uma determinada propriedade, o que ajuda bastante na expressividade.
Também penso dessa forma, procuro sempre criar Methods Extensions das entidades, assim retorno apenas a propriedades que forem nescessária.
Queria saber sua opinião sobre essa solução que acabei pensando sobre o problema:
Seguindo essa mesma ideia de visualizar os impactos de uma mudança de um objeto de domínio em uma API pública, no meu caso de uma API GraphQL, eu acabei pensando em um fluxo diferente. NOTA: são casos de APIs desenvolvidas em JS/Ruby.
É comum que o objeto de domínio na maioria dos casos simples (ao menos nas APIs que modelei), acabe refletindo exatamente o modelo do GraphQL. Nesses casos, acabo gerando o type do GraphQL baseado nesse modelo com metaprogramming. Em JS/Ruby, o custo disso é baixo.
O GraphQL permite gerar o schema da API, nesse caso eu salvo isso em um arquivo json. Eu uso esse arquivo para na etapa de CI/CD, buscar os metadados da API atual do GraphQL e efetuar um diff entre os schemas. Com isso, eu consigo gerar uma descrição amigável das diferenças de API que tiveram do código comparando os schemas. Tem várias maneiras de mostrar essa informação, aqui uma ideia disto: https://github.com/kamilkisiela/graphql-inspector
Dessa maneira, a pessoa visualiza claramente os impactos do código que vai ter por toda a API se mexer no código. E se ver necessário criar o type do GraphQL explicitamente, o faz.
E para passar no CI/CD e aceitar as novas mudanças na API, tem que mandar gerar novamente os metadados no JSON.
Vale lembrar que no caso de usar um github, tem como colocar um Github Bot para em todo o PR, efetuar um comentário com todas as diffs que o PR vai efetuar na API (e atualizar o comentário conforme novos commits/mudanças no PR).
Meu comentário à cerca do assunto é um tanto quanto ácido.
Quando passível dos riscos mencionados essa atitude em sua grande maioria é tomada conscientemente, justificado pela redução na complexidade e sempre apoiado em fatores externos.
Para os casos onde não há consciência do autor. Defendo que houve a distribuição de um serviço sem o devido domínio do modelo envolvido. O que mais uma vez sugere a criação de um novo modelo com liberação das propriedades sob tutela até garantir segurança na extensibilidade.