APIs
Design de API RestfulRESTful
Representational State Transfer (Transferência de Estado Representacional) ou REST, representa uma série de princípios definidos pela World Wide Web, visando a padronização de rotas, requisições e comunicações sem estado, o próprio HTTP, foi baseado nestas regras.
O protocolo HTTP é a base de tudo, as requisições chegam e saem através dele, os padrões são baseados em sua estrutura, e seus códigos de resposta, por esse motivo, devem respeitá-lo e aplicar suas melhores práticas.
Para uma boa API é ideal que o DEV saiba os conceitos básicos para aplicação do Restful, aqui você tem acesso a um breve artigo que resume o Restful na prática, mas recomendamos que busque por algum conteúdo mais avançado.
Verbos
Todas as APIs devem utilizar de forma correta e harmoniosa os verbos HTTP seguindo o Restful.
Retornos
Status Code
Os Status Code são extremamente importantes para que outros façam o tratamento do retorno da sua API, portanto é importante a boa utilização dos mesmos utilizando os padrões Restful.
Ninguém tem bola de cristal, então isto:
[HttpGet("{id}")]
public async Task<IActionResult> Get([FromRoute] Guid id)
{
...
if(!ok)
return BadRequest("Não foi possível realizar esta ação no momento!");
return Ok(objectDto);
}
é muito melhor que isto:
[HttpGet("{id}")]
public async Task<IActionResult> Get([FromRoute] GuiGuid id)
{
...
if(!ok)
return Ok("Não foi possível realizar esta ação no momento!");
return Ok(objectDto);
}
Exemplos de utilização
[ApiController]
[Route("api/[controller]]")]
public class ProductController : ControllerBase
{
[HttpGet("{id}")]
public async Task<IActionResult> Get([FromRoute] Guid id)
{
...
return Ok(objectDto);
}
[HttpPost]
public async Task<CreatedAtActionResult> Post([FromBody] PostRequest postRequest)
{
...
if(!ok)
return BadRequest("Não foi possível realizar esta ação no momento!");
return CreatedAtAction(nameof(Get), new { id }, new { id });
}
[HttpPut]
public async Task<CreatedAtActionResult> Put([FromBody] PutRequest putRequest)
{
...
if(!ok)
return BadRequest("Não foi possível realizar esta ação no momento!");
return CreatedAtAction(nameof(Get), new { id }, new { id });
}
[HttpDelete("{id}")]
public async Task<IActionResult> Delete([FromRoute] Guid id)
{
...
if(!ok)
return BadRequest("Não foi possível realizar esta ação no momento!");
return NoContent();
}
}
Objetos
Convenhamos que APIs não possuem Views, logo ViewModels não fazem sentido, pois as ViewModels representam dados exibidos em telas. Opte por utilizar Dtos e de preferência bem documentada utilizando annotations do próprio Swagger.
Exemplos de utilização
[SwaggerSchema(Required = new[] { nameof(Descricao) }]
public class ProdutoDto
{
[SwaggerSchema("O identificador do produto", ReadOnly = true)]
public int Id { get; set; }
[SwaggerSchema("A descrição do produto")]
public string Descricao { get; set; }
[SwaggerSchema("A data que o produto foi criado", Format = "date")]
public DateTime DataDeCriacao { get; set; }
}
Validação de Campos
Sempre use DataAnnotations
para validações simples de campos, como por exemplo:
public class ExemploDto
{
[Required(ErrorMessage = "Este campo é requirido!")]
public string CampoRequirido { get; set; }
[Display(Name = "Campo De 10 Caracteres Com Minimo De 4 Caracteres")]
[StringLength(10, ErrorMessage = "O {0} deve ter no mínimo {2} caracteres.", MinimumLength = 4)]
public string CampoDe10CaracteresComMinimoDe4Caracteres { get; set; }
[DataType(DataType.EmailAddress, ErrorMessage = "E-mail em formato inválido.")]
//OU
[EmailAddress(ErrorMessage = "E-mail em formato inválido.")]
public string CampoEmail { get; set; }
}
Outras annotations de validações conhecidas são:
- Required: Indica que a propriedade é um campo obrigatório
- StringLength: Define um comprimento máximo para o campo de string
- Range: Define um valor máximo e mínimo para um campo numérico
- RegularExpression: Especifica que o valor do campo deve corresponder à expressão regular especificada
- CreditCard: Especifica que o campo especificado é um número de cartão de crédito
- CustomValidation: Método de validação personalizada especificado para validar o campo
- EmailAddress: Valida com formato de endereço de e-mail
- FileExtension: Valida com extensão de arquivo
- MaxLength: Especifica o comprimento máximo para um campo de string
- MinLength: Especifica o comprimento mínimo para
- Phone: Especifica que o campo é um número de telefone que usa expressões regulares para números de telefone
NOTA: Para mais informações sobre o uso de cada annotation consulte a documentação oficial do namespace das DataAnnotations
Melhores Práticas para Projetar APIs RESTful
Use Substantivos em Vez de Verbos nas URLs
- Utilize substantivos para identificar recursos em vez de verbos. Por exemplo, "/Clientes" em vez de "/ObterClientes".
Utilize Verbos HTTP Adequados
- Escolha os verbos HTTP que melhor representam as operações no recurso. Por exemplo, use "POST" para criar um novo recurso e "PUT" para atualizar um recurso existente.
-
Evite incluir verbos nas URLs, uma vez que os verbos HTTP já definem a ação a ser realizada.
Exemplo:
Errado: /ObterProdutos Certo: /Produtos
Utilize Nomes Plurais
- Use nomes no plural para identificar recursos, pois isso ajuda a indicar que a URL representa uma coleção de recursos. Por exemplo, "/Produtos" em vez de "/Produto".
Versionamento de APIs
- Adicione a versão da API à URL para permitir a evolução controlada da API sem quebrar a compatibilidade com versões anteriores. Por exemplo, "/v1/Clientes".
Utilize Nomes Descritivos para Operações Específicas
Para operações que não se encaixam nos verbos HTTP padrão, utilize nomes descritivos nas URLs.
Exemplo:
POST /Produtos/Buscar
Segurança em APIs RESTful
Autenticação e Autorização
- Todas as APIs devem estar cadastradas em nosso API Management (atualmente a Central do Desenvolvedor) para garantir segurança e integração.
Padrão de nomenclatura de rotas
Padrão de Rotas em PascalCase para .NET
O padrão de rotas utilizando o PascalCase é amplamente adotado em APIs C# e segue as convenções da linguagem, tornando a leitura e manutenção do código mais natural para desenvolvedores que trabalham com C#.
O padrão de rotas em PascalCase consiste em utilizar letras iniciais em maiúsculas para cada palavra em uma URL. Não há separadores entre as palavras.
Por exemplo, se tivermos um recurso chamado "ProdutoCategoria", o padrão de rota será "ProdutoCategoria". Outro exemplo com múltiplas palavras é "ClienteDetalhes", representando um recurso de detalhes do cliente.
A adoção do padrão de rotas em PascalCase é uma prática comum em APIs .NET, alinhando-se com as convenções da linguagem e tornando o código mais legível e natural para os desenvolvedores.
Padrão de Rotas em kebab-case para outras linguagens
O padrão de rotas utilizando o kebab-case é amplamente adotado em APIs RESTful e segue a convenção de escrever as palavras em minúsculas e separadas por hífens. O kebab-case é uma escolha popular para URLs em APIs.
O padrão de rotas em kebab-case consiste em utilizar letras minúsculas para todas as palavras da URL e separar as palavras por hífens. Por exemplo, se tivermos um recurso chamado "ProdutoCategoria", o padrão de rota será "produto-categoria". Outro exemplo com múltiplas palavras é "cliente-detalhes", representando um recurso de detalhes do cliente.
Erros
IETF RFC 7807
O que é
O padrão RFC 7807 define o objeto "detalhes do problema" como uma forma de transportar erros legíveis em uma resposta HTTP para evitar a necessidade de definir novos formatos de resposta de erro para APIs HTTP.
Então, vamos resumir brevemente o objeto ProblemDetails
definido pela RFC 7807. É um formato JSON ou XML, que podemos usar para respostas de erro. Esse objeto nos ajuda a informar o cliente da API sobre detalhes de erros em um formato legível por máquina.
NOTA: Para mais informações acesse a documentação oficial do padrão
Sobre o ProblemDetails
O objeto ProblemDetails
é composto pelos seguintes campos:
- Type – [string] – referência de URI para identificar o tipo de problema
- Title – [string] – um breve resumo do problema legível por humanos
- Status – [int] – o código de status HTTP gerado na ocorrência do problema
- Detail – [string] – uma explicação legível para o que exatamente aconteceu
- Instance – [string] – referência URI da ocorrência
Adicionalmente no .NET é adicionado automaticamente o campo TraceId para facilitar o rastreio do problema
Body:
{
"type": "https://example.com/errors/produto-nao-existe",
"title": "Este produto não existe",
"status": 404,
"detail": "Este produto não existe ou não está disponível",
"instance": "/Produto/12345"
}
NOTA: Outros campos podem ser adicionados para complementar a informação
Headers:
content-type: application/problem+json; charset=utf-8
date: Tue20 Apr 2021 20:20:20 GMT
server: Kestrel
E no .NET?
Sobre o ProblemDetails
Desde o ASP.NET Core 2.2., o uso dos métodos da ControllerBase
para retornar as respostas do código de status HTTP, como o NotFound()
ou BadRequest()
, formata automaticamente a resposta como a classe ProblemDetails
. Isso é feito graças ao atributo [ApiController]
em nossas controllers.
Como uso?
Vamos primeiro explicar como funciona alguns dos métodos internos da ControllerBase
citados acima, vamos ter como exemplo o método BadRequest()
. Quando retornado sem parâmetros obtemos a seguinte resposta:
{
"type": "https://tools.ietf.org/html/rfc7231#section-6.5.1",
"title": "Bad Request",
"status": 400,
"traceId": "00-b3b83db7e1cf1774de979092a6074e17-fd3181056f6032e7-00"
}
Já com o método NotFound()
temos a seguinte resposta:
{
"type": "https://tools.ietf.org/html/rfc7231#section-6.5.4",
"title": "Not Found",
"status": 404,
"traceId": "00-f7e19d0b4e7d556940dafc116145092c-1596b8eece2437cf-00"
}
Podemos observar que eles utilizam o padrão RFC 7807, mas como o framework faz isso? Ele utiliza o próprio objeto ProblemDetails que é representado no ASP.NET pela classe ProblemDetails
e já simplificado como um método da ControllerBase
chamado Problem()
. Para ficar mais simples de entender vamos a mais exemplos
Código:
[HttpGet]
public IActionResult Get()
{
return Problem();
}
Resultado:
{
"type": "https://tools.ietf.org/html/rfc7231#section-6.5.1",
"title": "Bad Request",
"status": 400,
"traceId": "00-b3b83db7e1cf1774de979092a6074e17-fd3181056f6032e7-00"
}
Podemos observar que a resposta é praticamente a mesma dos métodos de respostas do código de status HTTP mudando apenas alguns dados, isso acontece porque eles utilizam o objeto ProblemDetails
.
Como fazemos para utilizar o padrão RFC 7807 caso eu queria modificar as informações do retorno? A resposta foi dada um pouco acima, a utilização do método Problem()
. Para entender melhor vamos a mais exemplos:
Código:
[HttpGet]
public IActionResult Get()
{
return Problem(detail: "Falhou aqui", statusCode: StatusCodes.Status400BadRequest, title: "Ocorreu um erro");
}
Resultado:
{
"type": "https://tools.ietf.org/html/rfc7231#section-6.5.1",
"title": "Ocorreu um erro",
"status": 400,
"detail": "Falhou aqui",
"traceId": "00-e255a8a890a3c4c762c02b2d693e2d0f-76a7d8992a077f6a-00"
}
O método Problem()
possui alguns parâmetros que permitem customizar o ProblemDetails
, os parâmetros são detail
,instance
, statusCode
, title
e type
.
Validações com DataAnnotations
As validações de modelo retornam automaticamente uma resposta adequada ao cliente. Os gatilhos para essas validações são atributos na definição do modelo, como [Required] e [EmailAdress] vistos em Validação de Campos. Quando acionamos essas validações, nossa API retorna respostas como a classe ValidationProblemDetails
, que herda da classe ProblemDetails
e adiciona a ela o campo errors
. Abaixo temos um exemplo de validação padrão baseada em DataAnnotations feita automaticamente pelo framework:
Código:
public class ExemploDto
{
[Required(ErrorMessage = "Nome é requirido")]
public string Nome { get; set; }
[MaxLength(20, ErrorMessage = "Categoria não pode ser maior que 20 caracteres")]
public string Categoria { get; set; }
}
Resultado:
{
"type": "https://tools.ietf.org/html/rfc7231#section-6.5.1",
"title": "One or more validation errors occurred.",
"status": 400,
"traceId": "00-5602408e68266c4d8e08a7c3cf397912-260d665978f08c47-00",
"errors": {
"Nome": [
"Nome é requirido"
],
"Categoria": [
"Categoria não pode ser maior que 20 caracteres"
]
}
}
Validações adicionais
Como ficam as validações que não podem ser feitas por DataAnnotations
? Nesses casos a solução também é simples, da mesma forma que temos um método que retorna o ProblemDetails, nós temos um para o ValidationProblemDetails citado acima. A utilização é simples, basta adicionar o erro no ModelState da requisição e utilizar o método ValidationProblem como vemos no exemplo abaixo:
Código:
[HttpGet]
public IActionResult Get()
{
ModelState.AddModelError("ValidacaoX", "X não atende a validação da regra de negócio");
return ValidationProblem(detail: "Falha de validação", title: "Falha de validação", modelStateDictionary: ModelState);
}
Resultado:
{
"type": "https://tools.ietf.org/html/rfc7231#section-6.5.1",
"title": "Falha de validação",
"status": 400,
"detail": "Falha de validação",
"traceId": "00-c4f047b277bc6f701e982d654c756997-8bbf5301c37d7ccb-00",
"errors": {
"ValidacaoX": [
"X não atende a validação da regra de negócio"
]
}
}
Documentação
Biblioteca de documentação
O Swagger será nossa documentação oficial, faça bom uso de seus recursos e capriche na sua documentação. Que tal dar uma olhadinha na documentação do Swagger para descobrir como se usa e conhecer suas funcionalidades?
Rota da documentação
Para facilitar a vida de todos, adotamos uma rota padrão /documentacao
para direcionar à documentação da API. Para realizar esta configuração basta inserir o código abaixo na configuração da UI do Swagger:
app.UseSwaggerUI(c =>
{
...
c.RoutePrefix = "documentacao";
...
});
Configuração quando usado em API Gateway
Ao utilizar um API Gateway, a configuração do Swagger não funciona normalmente pelo fato da aplicação estar em um diretório virtual, fazendo com que a UI do Swagger não encontre a fonte de configuração gerada por ele. Na imagem abaixo a API está no diretório /teste
do API Gateway, podemos ver que o Swagger não encontra seu arquivo de configuração.
Para resolver isso é preciso deixar explicíto na configuração da rota do arquivo de configuração que há um diretório ante a ele, para fazer isso basta inserir dois pontos (..
) antes do diretório como no exemplo abaixo:
app.UseSwaggerUI(c =>
{
...
c.SwaggerEndpoint("../swagger/v1/swagger.json", "Api v1");
...
});