HTTP e REST
📽 Veja esta aula no YouTube:
- 📺 Parte 1 - Introdução
- 📺 Parte 2 - Estrutura da aplicação
- 📺 Parte 3 - GET (listagem e filtro)
- 📺 Parte 4 - GET (item único)
- 📺 Parte 5 - DELETE
- 📺 Parte 6 - POST
- 📺 Parte 7 - PUT
- 📺 Parte 8 - PATCH
O HTTP (Hypertext Transfer Protocol) é o protocolo de comunicação sobre o qual a web funciona. Com ele navegadores, servidores, aplicativos mobile e qualquer outro tipo de aplicação podem trocar informações de maneira simples e direta.
Por exemplo, quando você quer acessar um site, você digita seu endereço (ou URL) em um navegador (cliente HTTP) e ele envia seu pedido (GET) para o servidor indicado na URL, que responde e esse resultado é exibido pelo navegador. Porém, HTTP é muito mais que isso, suportando muitos tipos de tráfego de informação.
Uma requisição (request) usa um método (method) ou verbo que indica a ação desejada e aponta para um URL (com um caminho de recurso, em um servidor). Pode conter um conjunto de cabeçalhos (headers) com a configuração da comunicação, e um corpo com informações adicionais. A resposta (response) possui um código de status indicando o sucesso/fracasso da comunicação, cabeçalhos opcionais, e o corpo da mensagem contendo o conteúdo requisitado.
HTTP Status Codes
Os códigos de status seguem uma tabela numérica, com o seguinte agrupamento:
- Respostas de informação (100-199)
- Respostas de sucesso (200-299)
- Redirecionamentos (300-399)
- Erros do cliente (400-499)
- Erros do servidor (500-599)
Por exemplo:
200 OK
caso a solicitação seja válida e o resultado seja enviado com sucesso404 NOT FOUND
caso o recurso não exista400 BAD REQUEST
caso a solicitação seja inválida (por erro do cliente)500 INTERNAL SERVER ERROR
caso ocorra um problema (por erro do servidor)
Veja uma tabela completa aqui. Veja também 🐱 aqui e 🐶 aqui.
Clientes HTTP
Usamos clientes HTTP toda vez que fazemos uma requisição a um servidor usando esse protocolo. O tipo mais conhecido é o navegador (browser), mas ele tem um comportamento com finalidade específica, e não serve para tudo que precisamos como desenvolvedor. Podemos fazer nossas chamadas manualmente com JavaScript usando Fetch, mas isso não é nada prático para testar as nossas comunicações com os backends.
Caso seja necessário baixe um cliente HTTP dedicado para desenvolvedores chamado Insomnia. Com ele podemos entender em detalhes o que acontece na comunicação. Baixe-o e instale-o acessando https://insomnia.rest/download/, opção Insomnia Core. Outra opção bastante utilizada é o Postman.
REST
Existe um estilo de arquitetura de sistemas criado para utilizar todo o potencial do protocolo HTTP chamado REST (REpresentational State Transfer). Ele é muito popular hoje em dia e o utilizaremos neste material.
O REST define regras e boas práticas para uso de HTTP em aplicações. Vejamos como usar os verbos, cabeçalhos, status e corpo das mensagens para integrar aplicações.
GET
Obtém do servidor um recurso único ou uma lista de recursos. Usado para buscar imagens ou arquivos HTML, e também objetos JSON com informações do backend.
Exemplos:
GET /Alunos
para buscar uma lista de alunos.GET /Alunos/98765
para buscar um aluno específico.
Resultados comuns:
200 OK
indica sucesso, com o resultado no corpo da mensagem (que pode ser vazio no caso da lista).404 NOT FOUND
em caso de não encontrar o recurso (único) solicitado.
Observações:
- Use query string para realizar filtros, ordenação e paginação. Esses parâmetros não são automáticos e devem ser tratados pelo backend um a um. Exemplos:
GET /Alunos?nome=Fabi
para filtrar a lista de alunos pelo nome.GET /Alunos?ordem=idade
para colocar em ordem de idade.GET /Alunos?inicio=11&quantidade=10
para colocar acessar a segunda página de 10 itens cada.
POST
Cria um recurso em uma lista de recursos. Os dados a serem cadastrados vão no corpo da requisição.
Exemplo:
POST /Alunos
para incluir um novo aluno na lista de alunos. Os dados a serem cadastrados devem ir no corpo da mensagem.
Resultados comuns:
200 OK
caso o recurso seja criado e não possua URL próprio.201 CREATED
caso o recurso seja criado e possua URL próprio. Nesse caso, o recurso criado volta no corpo da mensagem, e o seu URL no headerLocation
.400 BAD REQUEST
caso a solicitação seja inválida.
PUT
Substitui (altera) um recurso por inteiro. Deve ser chamado referenciando um recurso existente (como em GET
) e enviando os novos dados no corpo da mensagem (como em POST
).
Exemplos:
PUT /Alunos/98765
para alterar todos os dados de um aluno específico. Os novos dados a serem gravados devem ir no corpo da mensagem.
Resultados comuns:
200 OK
indica sucesso, com o resultado (recurso alterado) no corpo da mensagem.404 NOT FOUND
em caso de não encontrar o recurso solicitado.400 BAD REQUEST
caso a solicitação seja inválida.
PATCH
Altera/corrige parte de um registro.
Exemplos:
- Não há um consenso sobre a maneira correta de utilizar esse método. De maneira geral, é executado sobre um recurso único (com em
PATCH /Alunos/98765
), porém a maneira que os dados são recebidos e retornados é ajustado caso a caso.
Resultados comuns:
200 OK
indica sucesso.404 NOT FOUND
em caso de não encontrar o recurso solicitado.400 BAD REQUEST
caso a solicitação seja inválida.
DELETE
Exclui um recurso.
Exemplos:
DELETE /Alunos/98765
para excluir um aluno específico.
Resultados comuns:
200 OK
indica sucesso, com o corpo da mensagem vazio.404 NOT FOUND
em caso de não encontrar o recurso solicitado.
Exemplo de aplicação usado na aula
Vamos utilizar o banco de dados top5
contido aqui. Siga as instruções para criá-lo na sua máquina.
Inicie seu projeto webapi
chamando top5
, faça o scaffolding do banco e configure a aplicação para ler a string de conexão do arquivo appsettings.json e injetar o contexto. Todos esses passos estão nesta aula.
Vamos agora implementar nossa(s) controller(s). Se preferir acompanhar vendo o programa pronto, ele está disponível aqui.
Backend - Implementação das controllers
Vamos usar as recomendações REST para criar nossas controllers de API. Primeiramente, vamos criar a classe e prepará-la para obter o contexto do mecanismo de injeção de dependência.
Crie Controllers\TopsController.cs
com o namespace top5.Controllers
. Já adicione as referências utilizadas nessa aula. Configure-a a sua rota para que ela atenda a /api/Tops
.
Você deverá ter algo como isto:
using System;
using Microsoft.AspNetCore.Mvc;
using Microsoft.EntityFrameworkCore;
using System.Collections.Generic;
using System.Linq;
using top5.db;
namespace top5.Controllers
{
[ApiController]
[Route("api/[controller]")]
public class TopsController : ControllerBase
{
private readonly top5Context _db;
public TopsController(top5Context contexto)
{
_db = contexto;
}
}
}
Observações:
[Route("api/[controller]")]
define um padrãoapi/Tops
para todas as rotas atendidas por essa classe.private readonly top5Context _db
é uma maneira elegante de disponibilizar o contexto a todos os métodos da classe. Tem o mesmo uso queprivate top5Context _db { get; set; }
.
Feito isso, podemos começar a programar nossos métodos para responder às chamadas dos clientes REST nos endpoints desejados.
Neste exemplo, optamos por criar models somente quando necessário, usando as classes do Entity Framework quando possível.
Obtendo vários registros (listagens)
Precisamos de um endpoint para obter os nossos tops. Ela deve retornar uma lista de tops com todos os dados pertinentes.
Vamos atender a chamadas GET, e retornar um arranjo de objetos com todos os tops encontrados (ou uma lista vazia caso não exista nenhum). Vamos usar o código de retorno 200 OK
.
// O método atende a GET /api/Tops
[HttpGet]
public ActionResult<List<Top>> ObtemTops()
{
// Traz todos os tops do banco de dados
var tops = _db.Top.ToList<Top>();
// Retorna 200 OK com o resultado no corpo da mensagem
return Ok(tops);
}
[HttpGet]
indica que responderá ao verbo GET.- Ao retornar um
ActionResult<...>
podemos indicar o status HTTP da resposta. return Ok(tops)
indica um resultado200 OK
, com a listatops
convertida em JSON no corpo da mensagem.
Exemplo: GET /api/Tops
Caso não possua nenhum registro:
200 OK
[]
Caso possua registros:
200 OK
[
{
"id": "31319dcf-d281-4f92-a834-8aa166be2a9c",
"titulo": "Pratos Japoneses",
"curtidas": 0,
"item": []
},
{
"id": "eaf375a4-59e6-40c4-8ce3-2b58820b74f4",
"titulo": "Linguagens de Programação",
"curtidas": 0,
"item": []
}
]
Incluindo os dados dos itens
Podemos fazer os Include
s necessários para retornar objetos com múltiplos níveis no JSON.
var tops = _db.Top
.Include(top => top.Item)
.ToList<Top>();
Quando executamos, porém, nossa aplicação quebra e retorna a seguinte mensagem:
500 INTERNAL SERVER ERROR
System.Text.Json.JsonException: A possible object cycle was detected which is not supported. This can either be due to a cycle or if the object depth is larger than the maximum allowed depth of 32.
Isso se deve à referência circular existente entre tops (em Top.Item) e itens (em Item.Top). A biblioteca de conversão para JSON usada pelo ASP.NET não suporta essa situação por padrão. Devemos ativá-la.
Vamos instalar o pacote Microsoft.AspNetCore.Mvc.NewtonsoftJson
:
dotnet add package Microsoft.AspNetCore.Mvc.NewtonsoftJson
E trocar, em ConfigureServices
no arquivo Startup.cs
o comando…
services.AddControllers();
… por…
services.AddControllers().AddNewtonsoftJson(options =>
options.SerializerSettings.ReferenceLoopHandling = Newtonsoft.Json.ReferenceLoopHandling.Ignore
);
Agora ele ignora essa situação e funciona como esperado.
200 OK
[
{
"id": "31319dcf-d281-4f92-a834-8aa166be2a9c",
"titulo": "Pratos Japoneses",
"curtidas": 0,
"item": [
{
"topId": "31319dcf-d281-4f92-a834-8aa166be2a9c",
"posicao": 1,
"nome": "Temaki",
"curtidas": 0
},
{
"topId": "31319dcf-d281-4f92-a834-8aa166be2a9c",
"posicao": 2,
"nome": "Oniguiri",
"curtidas": 0
},
{
"topId": "31319dcf-d281-4f92-a834-8aa166be2a9c",
"posicao": 3,
"nome": "Sashimi",
"curtidas": 0
},
{
"topId": "31319dcf-d281-4f92-a834-8aa166be2a9c",
"posicao": 4,
"nome": "Tempura",
"curtidas": 0
},
{
"topId": "31319dcf-d281-4f92-a834-8aa166be2a9c",
"posicao": 5,
"nome": "Uramaki",
"curtidas": 0
}
]
},
{
"id": "eaf375a4-59e6-40c4-8ce3-2b58820b74f4",
"titulo": "Linguagens de Programação",
"curtidas": 0,
"item": [
{
"topId": "eaf375a4-59e6-40c4-8ce3-2b58820b74f4",
"posicao": 1,
"nome": "C#",
"curtidas": 0
},
{
"topId": "eaf375a4-59e6-40c4-8ce3-2b58820b74f4",
"posicao": 2,
"nome": "JavaScript",
"curtidas": 0
},
{
"topId": "eaf375a4-59e6-40c4-8ce3-2b58820b74f4",
"posicao": 3,
"nome": "Python",
"curtidas": 0
},
{
"topId": "eaf375a4-59e6-40c4-8ce3-2b58820b74f4",
"posicao": 4,
"nome": "Scala",
"curtidas": 0
},
{
"topId": "eaf375a4-59e6-40c4-8ce3-2b58820b74f4",
"posicao": 5,
"nome": "Elixir",
"curtidas": 0
}
]
}
]
Adicionando filtro
Queremos que nossa listagem de tops possa ser filtrada pelo seu título, exibindo somente os títulos que contenham um texto indicado pelo usuário. Parâmetros desse tipo devem ser enviado na URL no formato chamado de query string.
Ela vai após a rota iniciada por um ?
e formada por pares de chave e valor separadas por &
.
Formato: ?chave1=valor1&chave2=valor2&chave3=valor3
…
Exemplo: GET /api/Tops?titulo=jap
, indicando que o parâmetro titulo
possui o valor jap
.
Vamos usar isso para filtrar nossos tops.
// ...
public ActionResult<List<Top>> ObtemTops(string titulo) // Argumento
{
// ...
var tops = _db.Top
.Include(top => top.Item)
.Where(top => top.Titulo.Contains(titulo)) // Filtro
.ToList<Top>();
// ...
}
ObtemTops(string titulo)
indica que esse método possui um argumento chamadotitulo
que receberá o parâmetro de mesmo nome enviado pelo cliente.
Fazendo GET /api/Tops?titulo=jap
, temos uma lista com um único resultado:
200 OK
[
{
"id": "31319dcf-d281-4f92-a834-8aa166be2a9c",
"titulo": "Pratos Japoneses",
"curtidas": 0,
"item": [...]
}
]
Fazendo GET /api/Tops?titulo=X
, temos uma lista vazia:
200 OK
[]
Fazendo GET /api/Tops?titulo=a
, temos uma lista com vários resultados:
200 OK
[
{
"id": "31319dcf-d281-4f92-a834-8aa166be2a9c",
"titulo": "Pratos Japoneses",
"curtidas": 0,
"item": [...]
},
{
"id": "eaf375a4-59e6-40c4-8ce3-2b58820b74f4",
"titulo": "Linguagens de Programação",
"curtidas": 0,
"item": [...]
}
]
O problema é que uma chamada sem nenhum filtro retornará uma lista vazia, pois nenhum top possui título vazio. Precisamos torná-lo um argumento opcional.
Filtro opcional
Fazemos com que cada linha do banco seja incluída no resultado se não houver filtro ou se ela contiver o valor do filtro.
var tops = _db.Top
.Include(top => top.Item)
.Where(top => String.IsNullOrEmpty(titulo) || top.Titulo.Contains(titulo))
.ToList<Top>();
String.IsNullOrEmpty(variavelString)
retornatrue
somente sevariavelString
for vazia (""
) ou nula.
Código final
// GET api/Tops
// GET api/Tops?titulo=valorDesejado
[HttpGet]
public ActionResult<List<Top>> ObtemTops(string titulo)
{
// Obtém todos os tops que contém o título indicado
// ou todos, se não for indicado nenhum
var tops = _db.Top
.Include(top => top.Item)
.Where(top => String.IsNullOrEmpty(titulo) || top.Titulo.Contains(titulo))
.ToList<Top>();
// 200 OK
return Ok(tops);
}
Consulta a um registro único
Precisamos de um endpoint que retorne os dados de um registro único, caso já tenhamos o seu identificador. Para isso, passaremos o id
do top diretamente na rota solicitada. Só temos que indicar que o novo método que atenderá a rota saiba em que ponto da URL estará o valor do parâmetro.
Queremos atender a algo do tipo GET /api/Tops/identificador-do-registro
.
Nesse caso, precisamos responder 404 NOT FOUND
quando o registro não for encontrado.
// GET api/Tops/id-top-desejado
[HttpGet("{id}")]
public ActionResult<Top> ObtemTop(string id)
{
// Obtém um top que possua o id indicado
var top = _db.Top
.Include(top => top.Item)
.SingleOrDefault(top => top.Id == id);
if (top == null)
{
// 404 NOT FOUND
return NotFound();
}
// 200 OK
return Ok(top);
}
[HttpGet("{id}")]
eObtemTop(string id)
indicam que o valor indicado no final da rota será mapeado para o parâmetroid
.ActionResult<Top>
indica o retorno de um objeto único, e não uma lista..SingleOrDefault(top => top.Id == id)
filtra nossa lista peloId
retornando um registro único ou entãonull
.return NotFound()
produz404 NOT FOUND
.
Exemplo: GET /api/Tops/31319dcf-d281-4f92-a834-8aa166be2a9c
Caso não possua nenhum registro com esse id
:
404 NOT FOUND
(sem corpo de mensagem)
Caso possua o registro:
200 OK
{
"id": "31319dcf-d281-4f92-a834-8aa166be2a9c",
"titulo": "Pratos Japoneses",
"curtidas": 0,
"item": [
{
"topId": "31319dcf-d281-4f92-a834-8aa166be2a9c",
"posicao": 1,
"nome": "Temaki",
"curtidas": 0
},
{
"topId": "31319dcf-d281-4f92-a834-8aa166be2a9c",
"posicao": 2,
"nome": "Oniguiri",
"curtidas": 0
},
{
"topId": "31319dcf-d281-4f92-a834-8aa166be2a9c",
"posicao": 3,
"nome": "Sashimi",
"curtidas": 0
},
{
"topId": "31319dcf-d281-4f92-a834-8aa166be2a9c",
"posicao": 4,
"nome": "Tempura",
"curtidas": 0
},
{
"topId": "31319dcf-d281-4f92-a834-8aa166be2a9c",
"posicao": 5,
"nome": "Uramaki",
"curtidas": 0
}
]
}
Incluindo um registro
Esse endpoint usará o método POST para receber os dados a serem cadastrados através do corpo da mensagem (um objeto JSON).
O procedimento de inclusão exige alguns passos:
- validar os dados recebidos;
- incluir o registro no banco de dados com um
id
gerado pela aplicação; - retornar o objeto criado no corpo da mensagem;
- retornar o URL do objeto criado no header
Location
.
Vamos primeiro criar um método de validação que será usado para a inclusão e para a alteração. Ele receberá um Top
e retornará uma string descrevendo o erro, ou então uma string vazia caso não encontre erro. Perceba que o método é privado, e não responde a nenhum método/rota. Ele não pode ser chamado pela nossa API, somente pela nossa aplicação.
private string ValidaTop(Top topAValidar)
{
if (String.IsNullOrEmpty(topAValidar.Titulo))
{
return "Título não informado.";
}
if (topAValidar.Curtidas < 0)
{
return "Curtidas devem ser positivas.";
}
if (topAValidar.Item.Count() != 5)
{
return "São esperados exatos 5 itens.";
}
int posicaoEsperada = 1;
foreach(var item in topAValidar.Item.OrderBy(i => i.Posicao))
{
if (item.Posicao != posicaoEsperada)
{
return $"Não foi informado item {posicaoEsperada}.";
}
if (String.IsNullOrEmpty(item.Nome))
{
return $"Não foi informado o nome do item {item.Posicao}.";
}
if (item.Curtidas < 0)
{
return $"Curtidas do item {item.Posicao} devem ser positivas.";
}
posicaoEsperada++;
}
return "";
}
Usaremos esse método para verificar a consistência do registro. Abaixo, o código completo.
// POST api/Tops
// body: objeto do tipo Top
[HttpPost]
public ActionResult<Top> IncluiTop(Top topInformado)
{
if (topInformado.Id != null)
{
// 400 BAD REQUEST
return BadRequest(new { mensagem = "Id não pode ser informado." });
}
// Validação
var mensagemErro = ValidaTop(topInformado);
if (!String.IsNullOrEmpty(mensagemErro))
{
// 400 BAD REQUEST
return BadRequest(new { mensagem = mensagemErro });
}
// Gera novo identificador único
topInformado.Id = Guid.NewGuid().ToString();
// Salva o novo registro
_db.Add(topInformado);
_db.SaveChanges();
// 201 CREATED
// Location: url do novo registro
return CreatedAtAction(nameof(ObtemTop), new { id = topInformado.Id }, topInformado);
}
- ` [HttpPost]` indica que reponderá a chamadas POST.
IncluiTop(Top topInformado)
indica que o conteúdo JSON do corpo da requisição será convertido em um objeto do tipoTop
com o nometopInformado
.return BadRequest()
indica um retorno de status400 BAD REQUEST
vazio. Ao adicionar um objeto no retorno, ele será enviado no corpo da resposta.new { mensagem = mensagemErro }
indica um objeto anônimo (sem tipo definido), contendo a mensagem de erro.Guid.NewGuid().ToString()
gera uma string de 36 posições no formato GUID.return CreatedAtAction(ação, parâmetros, corpo)
indica um retorno201 CREATED
, com o headerLocation
preenchido com a URL do recurso criado (uma string dizendo a rota necessária para acessar ação com os valores deparâmetros
).
Exemplo: POST /api/Tops
, contendo no corpo da requisição:
{
"titulo": "Viagens mais desejadas",
"curtidas": 5,
"item": [
{
"posicao": 1,
"nome": "Londres"
},
{
"posicao": 3,
"nome": "Paris"
},
{
"posicao": 4,
"nome": "Santiago"
},
{
"posicao": 5,
"nome": "Gramado"
},
{
"posicao": 2,
"nome": "Nova Iorque"
}
]
}
Registro criado com sucesso:
201 CREATED
header: Location https://localhost:5001/api/Tops/b23366ff-bc4b-4f09-a75b-f07045322a1e
{
"id": "b23366ff-bc4b-4f09-a75b-f07045322a1e",
"titulo": "Viagens mais desejadas",
"curtidas": 5,
"item": [
{
"topId": "b23366ff-bc4b-4f09-a75b-f07045322a1e",
"posicao": 1,
"nome": "Londres",
"curtidas": 0
},
{
"topId": "b23366ff-bc4b-4f09-a75b-f07045322a1e",
"posicao": 3,
"nome": "Paris",
"curtidas": 0
},
{
"topId": "b23366ff-bc4b-4f09-a75b-f07045322a1e",
"posicao": 4,
"nome": "Santiago",
"curtidas": 0
},
{
"topId": "b23366ff-bc4b-4f09-a75b-f07045322a1e",
"posicao": 5,
"nome": "Gramado",
"curtidas": 0
},
{
"topId": "b23366ff-bc4b-4f09-a75b-f07045322a1e",
"posicao": 2,
"nome": "Nova Iorque",
"curtidas": 0
}
]
}
Dados inválidos (com -5 curtidas):
400 BAD REQUEST
{
"mensagem": "Curtidas devem ser positivas."
}
Alterando um registro (por inteiro)
Para alterar os dados de um registro, precisamos de um endpoint que aponte para um registro (como em GET) e receba os novos dados a serem gravados (como em POST).
O método usado é PUT, com o registro a ser alterado indicado na rota e os dados recebidos via corpo da mensagem. Retornará 200 OK
caso o registro esteja correto, 400 BAD REQUEST
para registros inválidos e 404 NOT FOUND
caso o registro solicitado não exista.
// PUT api/Tops/id-top-desejado
// body: objeto do tipo Top
[HttpPut("{id}")]
public ActionResult<Top> AlteraTop(string id, Top topAlterado)
{
if (topAlterado.Id != id)
{
// 400 BAD REQUEST
return BadRequest(new { mensagem = "Id inconsistente." });
}
// Obtém um top que possua o id indicado
var top = _db.Top
.Include(top => top.Item)
.SingleOrDefault(top => top.Id == id);
if (top == null)
{
// 404 NOT FOUND
return NotFound();
}
// Validação
var mensagemErro = ValidaTop(topAlterado);
if (!String.IsNullOrEmpty(mensagemErro))
{
// 400 BAD REQUEST
return BadRequest(new { mensagem = mensagemErro });
}
// Altera para os novos valores
top.Titulo = topAlterado.Titulo;
for(int posicao = 1; posicao <=5; posicao++)
{
string nomeAlterado = topAlterado.Item
.SingleOrDefault(i => i.Posicao == posicao)
.Nome;
top.Item
.SingleOrDefault(i => i.Posicao == posicao)
.Nome = nomeAlterado;
}
_db.SaveChanges();
// 200 OK
return Ok(top);
}
[HttpPut("{id}")]
eAlteraTop(string id, Top topAlterado)
indicam os parâmetrosid
(na rota) etopAlterado
(no corpo da mensagem).
Alterando parte de um registro
Quando necessitamos alterar ou corrigir somente parte de um registro, como em uma atualização de status, ou confirmação de uma ação, não usamos PUT e sim PATCH.
O recurso é indicado na rota, bem como a ação aser executada, e os dados pertinentes, quando existentes, no corpo da requisição. Retornará 200 OK
se a ação teve sucesso, ou 400 BAD REQUEST
nos demais casos.
Neste exemplo usamos PATCH quando o usuário curte um top ou um item de top. Retornamos o novo número de curtidas no corpo da resposta, em caso de sucesso. Para esse retorno, optamos pela criação de uma classe para definição de contrato de comunicação, sendo que todos os endpoints com a ação de curtir retornam o mesmo tipo de objeto (do tipo top5.Models.CurtidasModel
). Esse tipo de objeto é frequentemente chamado Data Transfer Object, ou DTO.
Veja a classe Model:
namespace top5.Models
{
public class CurtidasModel
{
public int Curtidas { get; set; }
}
}
Os métodos agora podem utilizá-la para definir o seu tipo de retorno.
Abaixo, o método que permite curtir um top. Ele verifica a sua existência, acrescenta um no número de curtidas e retorna o valor atualizado.
// PATCH api/Tops/id-top-desejado/curtir
[HttpPatch("{id}/curtir")]
public ActionResult<CurtidasModel> CurteTop(string id)
{
// Obtém um top que possua o id indicado
var top = _db.Top
.Include(top => top.Item)
.SingleOrDefault(top => top.Id == id);
if (top == null)
{
// 400 BAD REQUEST
return BadRequest();
}
// Acrescenta uma curtida
top.Curtidas += 1;
_db.SaveChanges();
// Retorna o novo número de curtidas
var retorno = new CurtidasModel { Curtidas = top.Curtidas };
// 200 OK
return Ok(retorno);
}
Exemplo: PATCH http://localhost:5000/api/tops/26fdcb96-ae06-4cf8-be91-62b50d944e32/curtir
Curtidas alteradas com sucesso:
200 OK
, com o novo número de curtidas
{
"curtidas": 12
}
Nos itens, recebemos na rota além do identificador do top, também a posição a ser curtida.
// PATCH api/Tops/id-top-desejado/Itens/posicao-desejada/curtir
[HttpPatch("{id}/Itens/{posicao}/curtir")]
public ActionResult<CurtidasModel> CurteItem(string id, int posicao)
{
// Obtém um top que possua o id indicado
var top = _db.Top
.Include(top => top.Item)
.SingleOrDefault(top => top.Id == id);
if (top == null)
{
// 400 BAD REQUEST
return BadRequest();
}
// Busca pelo item da posição indicada
var item = top.Item.SingleOrDefault(item => item.Posicao == posicao);
if (item == null)
{
// 400 BAD REQUEST
return BadRequest();
}
// Acrescenta uma curtida ao item
item.Curtidas += 1;
_db.SaveChanges();
// Retorna o novo número de curtidas
var retorno = new CurtidasModel { Curtidas = item.Curtidas };
// 200 OK
return Ok(retorno);
}
Exemplo: PATCH http://localhost:5000/api/tops/26fdcb96-ae06-4cf8-be91-62b50d944e32/itens/2/curtir
Curtidas alteradas com sucesso:
200 OK
, com o novo número de curtidas
{
"curtidas": 7
}
Top inexistente:
Exemplo (a): PATCH http://localhost:5000/api/tops/abc123/curtir
400 BAD REQUEST
Item inexistente:
Exemplo (b): PATCH http://localhost:5000/api/tops/26fdcb96-ae06-4cf8-be91-62b50d944e32/itens/-18/curtir
400 BAD REQUEST
{
"type": "https://tools.ietf.org/html/rfc7231#section-6.5.1",
"title": "Bad Request",
"status": 400,
"traceId": "|22f3a262-49620fc9d85708e9."
}
Excluindo um registro
A implementação da exclusão é muito parecida com a da consulta a um recurso único, com duas diferenças:
- O retorno não inclui dados do recurso (no exemplo,
ActionResult<Top>
se torna somenteActionResult
); - É efetuada a exclusão do recurso.
// DELETE api/Tops/id-top-desejado
[HttpDelete("{id}")]
public ActionResult ExcluiTop(string id)
{
// Obtém um top que possua o id indicado
var top = _db.Top
.Include(top => top.Item)
.SingleOrDefault(top => top.Id == id);
if (top == null)
{
// 404 NOT FOUND
return NotFound();
}
// Exclui todos os itens, e depois o top
top.Item.Clear();
_db.Remove(top);
_db.SaveChanges();
// 200 OK
return Ok();
}
Exclusão efetuada com sucesso:
Exemplo: DELETE http://localhost:5000/api/tops/26fdcb96-ae06-4cf8-be91-62b50d944e32
Não encontrado:
200 OK
Exemplo: DELETE http://localhost:5000/api/tops/xyz-3457686
404 NOT FOUND
Fetch de APIs REST
Precisamos agora estudar um pouco mais aprofundadamente a Fetch API para que possamos consumir o nosso backend em todas as suas nuances.
A função fetch
pode receber um segundo parâmetro indicando as opções da requisição (request): fetch(url, request)
. Esse objeto vai conter as configurações e os dados a serem enviados na requisição:
method
indica o método a ser utilizado, comoGET
ouPOST
.headers
contém um objeto cujas propriedades serão enviadas no cabeçalho da requisição.body
contém uma string ou campos de formulário enviados no corpo da requisição.
Chamadas REST
GET
, sem parâmetro de query string:
// ...
fetch("/api/Tops")
// ...
GET
, com parâmetro de query string:
// ...
fetch(`/api/Tops?titulo=${tituloDesejado}`)
// ...
POST
:
// ...
fetch("/api/Tops", {
method: "POST",
headers: {
"Content-Type": "application/json",
},
body: JSON.stringify(novoTop),
})
// ...
PUT
:
// ...
fetch(`/api/Tops/${id}`, {
method: "PUT",
headers: {
"Content-Type": "application/json",
},
body: JSON.stringify(topAlterado),
})
// ...
PATCH
:
// ...
fetch(`/api/Tops/${id}/Itens/${posicao}/curtir`, { method: "PATCH" })
// ...
DELETE
:
// ...
fetch(`/api/Tops/${id}`, { method: "DELETE" })
// ...
Entendendo os resultados
Após a requisição, o objeto retornado possui todo o conteúdo da resposta.
.status
possui o código de status do retorno (ex.:404
);.statusText
possui a descrição textual do status de retorno (ex.:NOT FOUND
);.ok
étrue
se o resultado possui status de sucesso (entre 200 e 299, inclusive);.json()
obtém um objeto JavaScript equivalente ao conteúdo JSON recebido.
Exemplo:
// ...
const response = await fetch(url, requestOptions);
if (response.ok) {
// Sucesso
const result = await response.json();
// ...
} else {
// Erro
alert(`Erro: ${response.status} - ${response.statusText}`);
}
// ...
Código completo
Complemente seus estudos vendo o programa pronto e estudando seu conteúdo. Ele está disponível aqui.