Neste artigo veremos algumas recomendações práticas e básicas para a criação de Web APIs com ASP.NET Core. |
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...
"Porque todos sois filhos de Deus pela fé
em Cristo Jesus.Porque todos quantos fostes batizados em Cristo já vos
revestistes de Cristo."
Gálatas 3:26,27
Referências: