APIs
Design de API Restful
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] Gui 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
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");
...
});