Ir para o conteúdo principal

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.

image.pngimage.png

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");
    ...
});