.NET

20 abr, 2023

Recomendações práticas e básicas para a criação de Web APIs com ASP.NET Core

Publicidade

As APIs REST permitem que vários clientes, incluindo navegador, aplicativos de desktop, aplicativos móveis e basicamente qualquer dispositivo com conexão à Internet, se comuniquem com um servidor. Portanto, é muito importante projetar APIs REST adequadamente para que não tenhamos problemas no futuro.

Para criar uma API robusta existem muitos detalhes e muitos fatores que devemos considerar, desde a segurança básica até o uso dos métodos HTTP corretos, implementando a autenticação, decidindo quais solicitações e respostas são aceitas e retornadas, entre muitos outros.

Assim veremos algumas recomendações práticas sobre o que faz uma boa API. Todas as dicas são independentes de linguagem, portanto, elas se aplicam potencialmente a qualquer estrutura ou tecnologia. Então vamos ao que interessa.

1. Prefira usar substantivos nos caminhos dos endpoint

Devemos considerar o uso de substantivos que representam a entidade que estamos recuperando ou manipulando como o nome do caminho e sempre a favor do uso de designações no plural.Evite usar verbos nos caminhos de endpoint porque nosso método de request HTTP já tem o verbo e isso não adiciona nenhuma informação nova.

A ação deve ser indicada pelo método de solicitação HTTP que estamos fazendo. Os métodos mais comuns são GET, POST, PATCH, PUT e DELETE.

– GET recupera recursos.
– POST cria um novo recurso no servidor.
– PUT/PATCH atualiza um recurso existente.
– DELETE remove o recurso

Os verbos mapeiam para operações CRUD.

Com esses princípios em mente, devemos criar rotas como GET /livros para obter uma lista de livros e não GET /obter-livros nem GET /livro. Da mesma forma, POST /livros adiciona um novo livro, PUT /livros/{id} atualiza os dados completos do livro com um determinado id, enquanto PATCH /livros/{id} faz alterações parciais no livro. Finalmente, DELETE /livros/{id} deleta um livro existente com o ID fornecido.

Não é proibido usar verbos nem nomes no singular;  uma forma de agradar a todos seria definir a seguinte regra: ‘Seja consistente com a escolha que você fez’

Assim, se você usar os nomes no singular faça isso em toda a sua API, seja consistente.

2. JSON é o formato principal para envio e recebimento de dados

Aceitar e responder a solicitações de API era feito principalmente em XML até alguns anos atrás. Mas hoje em dia, JSON (JavaScript Object Notation) tornou-se amplamente o formato “padrão” para enviar e receber dados de API na maioria dos aplicativos. Portanto, a recomendação é garantir que nossos endpoints retornem o formato de dados JSON como resposta e também ao aceitar informações por meio do payload de mensagens HTTP.

Embora Form Data seja bom para enviar dados do cliente, principalmente se quisermos enviar arquivos, não é ideal para texto e números. Não precisamos de dados de formulário para transferi-los, pois com a maioria dos frameworks podemos transferir JSON diretamente no lado do cliente.

Ao receber dados do cliente, precisamos garantir que o cliente interprete os dados JSON corretamente e, para isso, o tipo Content-Type no cabeçalho de resposta deve ser definido como application/json ao fazer a solicitação.

Vale a pena mencionar mais uma vez a exceção se estivermos tentando enviar e receber arquivos entre cliente e servidor. Para este caso específico, precisamos lidar com respostas de arquivos e enviar dados de formulário do cliente para o servidor.

3. Use um conjunto de códigos de status HTTP previsíveis

É sempre uma boa ideia usar códigos de status HTTP de acordo com suas definições para indicar o sucesso ou a falha de uma solicitação. Use os mesmos códigos de status para os mesmos resultados em toda a API. Alguns exemplos são:

200 para o sucesso geral
201 para uma criação bem-sucedida
400 para solicitações inválidas do cliente, como parâmetros inválidos
401 para solicitações não autorizadas
403 para permissões ausentes nos recursos
404 para recursos ausentes
429 para muitos pedidos
5xx  para erros internos (estes devem ser evitados tanto quanto possível)

Pode haver mais códigos de status dependendo do seu caso de uso, mas limitar a quantidade de código de status ajuda o cliente a consumir uma API mais previsível.

4. Retornar mensagens padronizadas

Além do uso de códigos de status HTTP que indicam o resultado da solicitação, sempre use respostas padronizadas para endpoints semelhantes. Os consumidores podem sempre esperar a mesma estrutura e agir em conformidade. Isso também se aplica a mensagens de sucesso  e mensagens de erro também.

No caso de buscar coleções, mantenha um formato específico, caso o corpo da resposta inclua uma matriz de dados como este:

[
  {
     livroId: 1,
     nome: "O alienista"
  },
  {
     livroId: 2,
     nome: "A coisa"
  }
]

Ou um objeto combinado como este:

{
   "dados": [ 
   {
       livroId: 1,
       nome: "O alienista"
     },
     {
     livroId: 2,
     nome: "A coisa"
     }
 ],
 "totalDocs": 200,
 "nextPageId": 3
}

O conselho é ser consistente, independentemente da abordagem que você escolher para isso. O mesmo comportamento deve ser implementado ao buscar um objeto e também ao criar e atualizar recursos para os quais geralmente é uma boa ideia retornar a última instância do objeto.

Embora não prejudique, é redundante incluir uma mensagem genérica como “Livro criado com sucesso”, pois isso está implícito no código de status HTTP.

Por último,  os códigos de erro são ainda mais importantes quando se tem um formato de resposta padrão. Esta mensagem deve incluir informações que um cliente pode usar para apresentar erros ao usuário final, não um alerta genérico como “Algo deu errado” que devemos evitar ao máximo. Aqui está um exemplo:

{
“code”: “livro/not_found”,
“message”: “Não foi possível encontrar um livro com o ID 6”
}


Novamente, não é necessário incluir o código de status no conteúdo da resposta, mas é útil definir um conjunto de códigos de erro como livro/not_found para que o consumidor os mapeie para diferentes strings e decida sua própria mensagem de erro para o usuário.

Em particular para ambientes de desenvolvimento, pode parecer adequado incluir também a pilha de erros na resposta para ajudar na depuração de bugs.

5. Use paginação, filtragem e classificação ao buscar coleções de registros

Assim que construirmos um endpoint que retorne uma lista de itens, a paginação deve ser colocada em prática. As coleções geralmente rapidamente, portanto, é importante sempre retornar uma quantidade limitada e controlada de elementos.

É justo permitir que os consumidores da API escolham quantos objetos obter, mas é sempre uma boa ideia predefinir um número e ter um máximo para ele. A principal razão para isso é que consumirá muito tempo e largura de banda para retornar uma enorme variedade de dados.

Para implementar a paginação, existem duas maneiras bem conhecidas de fazê-lo: skip/limit ou usar keyset.

A primeira opção permite uma maneira mais amigável de buscar dados, mas geralmente tem menos desempenho, já que os bancos de dados terão que varrer muitos documentos ao buscar registros “bottom line”. Por outro lado, a paginação usando o conjunto de chaves recebe um identificador/id como referência para “cortar” uma coleção ou tabela com uma condição sem escanear registros.

Na mesma linha de pensamento, as APIs devem fornecer filtros e recursos de classificação que enriqueçam a forma como os dados são obtidos. Para melhorar o desempenho, os índices de banco de dados fazem parte da solução para maximizar o desempenho com os padrões de acesso que são aplicados por meio desses filtros e opções de classificação.

Como parte do design da API, essas propriedades de paginação, filtragem e classificação são definidas como parâmetros de consulta na URL. Por exemplo, se quisermos obter os primeiros 10 livros que pertencem a uma categoria “romance”, nosso endpoint ficaria assim:

GET /livros?limite=10&categoria=romance

6. Considere usar PATCH em vez de PUT

É muito improvável que tenhamos a necessidade de atualizar totalmente um registro completo de uma só vez, geralmente há dados confidenciais ou complexos que queremos manter fora da manipulação do usuário.

Com isso em mente, as solicitações PATCH devem ser usadas para realizar atualizações parciais em um recurso, enquanto PUT substitui um recurso existente inteiramente. Ambos devem utilizar o corpo da solicitação para passar as informações a serem atualizadas.

Assim para modificar um campo específico a recomendação  é usar PATCH enquanto que modificar o  objeto completo usa solicitações PUT. No entanto, vale a pena mencionar que nada nos impede de usar PUT para atualizações parciais, não há “restrições de transferência de rede” que validem isso, é apenas uma convenção que é uma boa ideia seguir.

7. Forneça opções de resposta estendidas

Os padrões de acesso são fundamentais ao criar os recursos de API disponíveis e quais dados são retornados. Quando um sistema cresce, as propriedades de registro também crescem nesse processo, mas nem todas essas propriedades são sempre necessárias para que os clientes operem.

É nessas situações que fornecer a capacidade de retornar respostas reduzidas ou completas para o mesmo endpoint se torna útil. Se o consumidor precisar apenas de alguns campos básicos, ter uma resposta simplificada ajuda a reduzir o consumo de largura de banda e potencialmente a complexidade de buscar outros campos calculados.

Uma maneira fácil de abordar esse recurso é fornecer um parâmetro de consulta extra para habilitar/desabilitar o fornecimento da resposta estendida.

GET /livros/:id
{
“livroId”: 1,
“nome”: “O Alienista”
}

GET /livros/:id?extended=true
{
“livroId”: 1,
“nome”: “O Alienista”
“tags”: [“conto”, “novela”],
“autor”: {
“id”: 1,
“nome”: “Machado de Assins”
}

 

8. Responsabilidade do endpoint

O Princípio da Responsabilidade Única (SRP) concentra-se no conceito de manter uma função, método ou classe, focado em um comportamento restrito e fazer isso bem. Quando pensamos em uma determinada API, podemos dizer que uma boa API uma coisa e nunca muda.

Isso ajuda os consumidores a entender melhor nossa API e torná-la previsível, o que facilita a integração geral. É preferível estender nossa lista de endpoints disponíveis para ser mais total, em vez de criar endpoints muito complexos que tentam resolver muitas coisas ao mesmo tempo.

9. Forneça uma boa documentação para a API

Os consumidores de sua API devem ser capazes de entender como usar e o que esperar dos endpoints disponíveis. Isso só é possível com uma documentação boa e detalhada. Tenha em consideração os seguintes aspectos para fornecer uma API bem documentada.

– Endpoints disponíveis descrevendo o propósito deles;
– Permissões necessárias para executar um endpoint;
– Exemplos de invocação e resposta;
– Mensagens de erro a esperar;

A outra parte importante para que isso seja um sucesso é ter a documentação sempre atualizada após as alterações e acréscimos do sistema. A melhor maneira de conseguir isso é tornar a documentação da API uma peça fundamental do desenvolvimento.

Duas ferramentas bem conhecidas a esse respeito são Swagger e Postman, que estão disponíveis para a maioria das estruturas de desenvolvimento de API existentes.

10. Use SSL para segurança e configure o CORS

Segurança, outra propriedade fundamental que nossa API deve ter. Configurar o SSL instalando um certificado válido no servidor garantirá uma comunicação segura com os consumidores e evitará vários ataques potenciais.

O CORS (compartilhamento de recursos de origem cruzada) é um recurso de segurança do navegador que restringe solicitações HTTP de origem cruzada iniciadas a partir de scripts em execução no navegador. Se os recursos da sua API REST receberem solicitações HTTP não simples de origem cruzada, você precisará habilitar o suporte ao CORS para que os consumidores operem de acordo.

O protocolo CORS exige que o navegador envie uma solicitação de comprovação ao servidor e aguarde a aprovação (ou uma solicitação de credenciais) do servidor antes de enviar a solicitação real. A solicitação de comprovação aparece na API como uma solicitação HTTP que usa o método OPTIONS (entre outros cabeçalhos).

Portanto, para dar suporte ao CORS, um recurso da API REST precisa implementar um método OPTIONS que possa responder à solicitação de simulação OPTIONS com pelo menos os seguintes cabeçalhos de resposta exigidos pelo padrão Fetch:

Access-Control-Allow-Methods
Access-Control-Allow-Headers
Access-Control-Allow-Origin

Quais valores atribuir a essas chaves dependerão de quão aberta e flexível queremos que nossa API seja. Podemos atribuir métodos específicos e origens conhecidas ou usar curingas para ter restrições de CORS abertas.

11. Versão da API

Como parte do processo de evolução do desenvolvimento, os endpoints começam a mudar e são reconstruídos. Mas devemos evitar tanto quanto possível a mudança repentina de endpoints para os consumidores. É uma boa ideia pensar na API como um recurso compatível com versões anteriores, onde endpoints novos e atualizados devem ser disponibilizados sem afetar os padrões anteriores.

É aqui que o controle de versão da API se torna útil, onde os clientes devem poder selecionar a qual versão se conectar. Há várias maneiras de declarar o controle de versão da API:

1. Adicionar um novo cabeçalho “x-version=v2”
2. Ter um parâmetro de consulta “?apiVersion=2”
3. Tornar a versão parte do URL: “/v2/livros/:id”

Entrar em detalhes sobre qual abordagem é mais conveniente, quando oficializar uma nova versão e quando descontinuar versões antigas são certamente perguntas interessantes a serem feitas, para mais detalhes sobre o versionamento de uma API veja o artigo : Versionando sua API.

12. Use o cache de dados para melhorar o desempenho

Para ajudar no desempenho de nossa API, é benéfico ficar de olho em dados que raramente mudam e são acessados com frequência. Para este tipo de dados podemos considerar o uso de um banco de dados in-memory ou cache que evita o acesso ao banco de dados principal.

O principal desafio com essa abordagem é que os dados podem ficar desatualizados, portanto, um processo para implementar a versão mais recente também deve ser considerado.

O uso de dados em cache será útil para os consumidores carregarem configurações e catálogos de informações que não soferm alterações frequentes. Ao usar o cache, certifique-se de incluir as informações de Cache-Control nos cabeçalhos. Isso ajudará os usuários a usar efetivamente o sistema de cache.

13. Use datas UTC padrão

No nível de dados, é importante ser consistente em como as datas são exibidas para aplicativos cliente.

A ISO 8601 é o formato padrão internacional para dados relacionados a data e hora. As datas devem estar no formato “Z” ou UTC, a partir do qual os clientes podem decidir um fuso horário para ela, caso essa data precise ser exibida sob quaisquer condições, segue um exemplo a seguir:

{
“createdAt”: “2022-03-08T19:15:08Z”
}

14. Forneça um endpoint de verificação de integridade

Pode haver momentos difíceis em que nossa API esteja inativa e pode levar algum tempo para colocá-la em funcionamento. Nestas circunstâncias, os clientes gostariam de saber que os serviços não estão disponíveis para que possam estar cientes da situação e agir em conformidade.

Para conseguir isso, forneça um endpoint (como GET /verificacao) que determine se a API está íntegra ou não. Esse endpoint pode ser chamado por outros aplicativos, como balanceadores de carga. Podemos até dar um passo adiante e informar sobre períodos de manutenção ou condições de saúde em partes da API.

15. Aceite a autenticação de chave de API

Permitir autenticação por meio de chaves de API oferece a capacidade de aplicativos de terceiros criarem facilmente uma integração com nossa API.

Essas chaves de API devem ser passadas usando um cabeçalho HTTP personalizado (como Api-Key ou X-Api-Key). As chaves devem ter uma data de validade e deve ser possível revogá-las para que possam ser invalidadas por motivos de segurança.

E estamos conversados…

 

*O conteúdo deste artigo é de responsabilidade do(a) autor(a) e não reflete necessariamente a opinião do iMasters.