Definindo os endpoints da Biblioteca
Como definimos anteriormente, o escopo do nosso projeto determina que nós devemos escrever uma API REST onde seja possível:
- Visualizar os livros da biblioteca disponíveis no sistema.
- Cadastrar, editar e remover livros no sistema.
Para tanto, devemos criar, então, os endpoints necessários para que o nosso sistema seja capaz de realizar estas ações. Mas antes de tudo, vamos recapitular o que é uma API e o que é o protocolo REST.
O que é uma API
Uma API (Application Program Interface) é uma interface de comunicação entre softwares.
Pense em uma interface de usuário. Uma interface de usuário serve como meio de comunicação entre o usuário e o software. Já uma API é um meio de comunicação entre softwares.
Você utiliza APIs constantemente quando escreve um programa. Quando você utiliza alguma função da biblioteca padrão da sua linguagem favorita, você está consumindo uma API.
Por exemplo, o C# tem uma API para a manipulação da entrada e saída de um programa que é executado em um terminal:
a classe System.Console.
Quando você usa Console.WriteLine para exibir um "Hello World!" no terminal, você não precisa se preocupar com as operações necessárias para mostrar uma mensagem no console. Você apenas chama o método com a mensagem que quer exibir. Estes detalhes internos são abstraídos para você em uma
única chamada de um método. APIs são formas de criar abstrações de funcionalidades.
Se você precisasse desenvolver um sistema desktop, como você o faria? Para criar uma janela no sistema operacional Windows, você precisaria utilizar as APIs do Win32. No Linux, para fazer a mesma coisa, você precisaria utilizar as APIs há o X.Org ou do Wayland. Naturalmente, cada sistema operacional diferente possui APIs diferentes para criar interfaces gráficas. Porém, você gostaria que sua aplicação fosse multiplataforma, mas para que isso aconteça, você teria que escrever códigos diferentes que utilizariam cada API de cada sistema operacional. Você poderia pesquisar uma biblioteca ou framework que facilita esse processo, abstraindo os detalhes internos de cada API nativa dos sistemas operacionais em uma única API. Seu progama, então, utiliza este framework para mostrar uma interface, que por sua vez, internamente, utiliza as APIs nativas do sistema operacional onde o seu software está sendo executado. Já os sistemas operacionais por sua vez utilizam as APIs do seu driver de vídeo para exibir a interface no seu monitor, as APIs do controlador USB para ler eventos de teclado e mouse, entre outros. Ou seja, com APIs podemos criar várias camadas de abstração. Todo o ambiente em um computador é composto por softwares comunicando-se uns com os outros, utilizando estas abstrações, através de APIs.
Há inúmeras formas de se montar uma API. Nos exemplos anteriores, isso acontece através de bibliotecas. Formas de comunicação inter-processo, como sockets, também podem ser utilizadas. Desta forma, podemos utilizar também protocolos de rede, como o próprio HTTP. Uma API que funciona no protocolo HTTP é uma API Web.
Como o HTTP é um protocolo de rede para a comunicação entre dois computadores distintos, o software que consome a API e o que a disponibiliza não precisam estar no mesmo computador. Temos, então, um sistema distribuído ou descentralizado.
O padrão REST
O REST é um padrão arquitetural para aplicações em rede que utilizam o protocolo HTTP. Este padrão impõe algumas restrições para o desenho de APIs Web de forma a melhorar a qualidade e a confiabilidade. Este padrão foi descrito em 2000 na dissertação de PHD de Roy Fielding e é ubíquo até hoje na arquitetura de sistemas Web.
REST significa Representational State Transfer (transferência de estado representacional). Podemos entendê-lo, de uma forma muito simplificada (e que não compreende todo o padrão como foi escrito na dissertação), como uma série de regras para usarmos o protocolo HTTP para acessar ou manipular recursos em um servidor, onde o identificador do recurso é a URI, o verbo HTTP define a ação que se deseja realizar sobre o recurso, o status de resposta (status code) define o estado da operação, e o conteúdo (corpo) da mensagem é a representação do recurso.
Voltando ao escopo da API que queremos desenvolver, nós queremos visualizar os livros disponíveis na biblioteca. Para isso, utilizando o padrão REST, podemos criar alguns endpoints como:
GET /livros O verbo é um GET e a URI é /livros. Isto significa que queremos ler o recurso chamado livros. Uma requisição para este endpoint deveria, portanto, ser respondida com uma lista
de livros disponíveis.
Request:
GET /livros
---
Response:
200 Ok
Andrew S. Tanembaum - Distributed Systems 4th Edition
Thomas H. Cormen - Introduction to Algorithms
Donald Knuth - The Art of Computer ProgrammingE se desejássemos obter um único livro, identificado pela sua chave primária. Poderíamos fazer um endpoint:
Request:
GET /livros/9788574481371
---
Response:
200 Ok
Yasunari Kawabata - Contos da Palma da Mão Note que agora a URI ficou /livros/9788574481371 onde 9788574481371 é a chave primária do livro, o dado que o identifica.
E como poderíamos representar o estado de um recurso? Podemos utilizar formatos específicos de dados, como o XML:
Request:
GET /livros/9788574481371
---
Response:
200 OK
<livro>
<id>9788574481371</id>
<titulo>Contos da Palma da Mão</titulo>
<autor>Yasunari Kawabata</autor>
</livro> Ou o JSON, que é o mais comumente utilizado hoje em dia:
Request:
GET /livros/9788574481371
Response:
200 Ok
{
"id": "9788574481371",
"titulo": "Contos da Palma da Mão",
"autor": "Yasunari Kawabata"
} E se quiséssemos cadastrar um livro, como poderíamos definir um endpoint que realizaria esta ação?
Request:
POST /livros
{
"titulo": "Structure and Interpretation of Computer Programs",
"autor": "Gerald Jay Sussman"
}
Response:
201 Created
{
"id": "9780262510875",
"titulo": "Structure and Interpretation of Computer Programs",
"autor": "Gerald Jay Sussman"
} O verbo POST em uma requisição indica que desejamos inserir um recurso. No nosso caso, POST /livros indica que queremos criar um novo registro de livro.
Note que enviamos na requisição os dados do livro que desejamos inserir.
Se quisermos agora editar este mesmo livro que acabamos de inserir, podemos utilizar uma requisição com verbo PUT:
Request:
PUT /livros/9780262510875
{
"id": "9780262510875",
"titulo": "Structure and Interpretation of Computer Programs, 2nd ed.",
"autor": "Harold Abelson, Gerald Jay Sussman, Julie Sussman"
}
Response:
200 Ok
{
"id": "9780262510875",
"titulo": "Structure and Interpretation of Computer Programs, 2nd ed.",
"autor": "Harold Abelson, Gerald Jay Sussman, Julie Sussman"
} Assim como no endpoint que obtém os dados de um livro, este também recebe na url o dado que identifica o registro, para indicar qual livro queremos editar.
Comumente utiliza-se o verbo POST para criar, e o verbo PUT para editar. Uma diferença conceitual entre os dois verbos é que o POST é utilizado em operações que não são idempotentes.
Idempotência é uma propriedade de uma operação que define que a aplicação sucessiva da mesma operação não altera o estado do sistema. Por exemplo, se você mandar a mesma requisição PUT para editar um livro várias vezes seguidas,
com exatamente os mesmos dados, o estado daquele registro de livro manterá-se exatamente igual.
Já no POST, enviar várias requisições idênticas, com os mesmos dados, geraria vários registros de livros diferentes, pois cada requisição representa uma inserção (ou então, um erro de validação de dados seria retornado, por exemplo, informando que já existe
um livro com o mesmo ISBN cadastrado).
Por fim, para remover um livro, podemos utilizar o verbo DELETE:
Request:
DELETE /livros/9788574481371
Response:
204 No Content Este endpoint também recebe o dado que identifica o livro, assim definimos qual registro queremos remover. Normalmente, endpoints com verbo DELETE não possuem corpo na requisição, e têm respostas vazias.
Definindo os endpoints na API
Temos então que os endpoints que precisamos implementar são:
GET /livrospara obter todos os livros no sistema;GET /livros/{id}para obter um único livro, identificado pela sua chave primária;POST /livrospara inserir um novo livro;PUT /livros/{id}para editar um livro;DELETE /livros/{id}para remover um livro.
Vamos traduzir esta especificação para o código. Abra seu projeto, no arquivo Program.cs. No exemplo anterior, deixamos este arquivo assim:
var builder = WebApplication.CreateBuilder(args);
var app = builder.Build();
app.MapGet("/", () =>
{
string[] saudacoes = ["Olá!", "こんにちは", "Привет", "Ողջույն"];
return saudacoes[Random.Shared.Next(saudacoes.Length)];
});
app.Run(); Definimos um endpoint GET / que responde com uma mensagem aleatória. Vamos utilizar a mesma sintaxe para definir os outros endpoints. Mas antes, vamos criar, neste mesmo arquivo, uma classe que irá representar um livro.
var builder = WebApplication.CreateBuilder(args);
var app = builder.Build();
app.MapGet("/", () =>
{
string[] saudacoes = ["Olá!", "こんにちは", "Привет", "Ողջույն"];
return saudacoes[Random.Shared.Next(saudacoes.Length)];
});
app.Run();
public class Livro
{
public long Id { get; set; }
public string? Isbn { get; set; }
public string? Titulo { get; set; }
public string? Autor { get; set; }
}; Adicione esta classe no fim do arquivo. Todos os campos, com exceção do Id, são do tipo string?. O ? após o tipo determina a nulabilidade daquela propriedade - ou seja, determina se a propriedade pode receber valores nulos.
Desta forma, o compilador consegue te auxiliar gerando avisos quando você faz uma referência a uma variável que pode ser nula sem antes verificá-la, o que, se acontecesse, geraria um erro em tempo de execução.
Agora, abaixo do MapGet("/", ...), escreva:
app.MapGet("/", () =>
{
...
});
// Obtém uma lista com os livros registrados.
app.MapGet("/livros", () =>
{
return new Livro[]
{
new()
{
Id = 1,
Isbn = "9780262510875",
Titulo = "Structure and Interpretation of Computer Programs",
Autor = "Gerald Jay Sussman"
},
new()
{
Id = 2,
Isbn = "9780131103627",
Titulo = "C Programming Language: ANSI C Version",
Autor = "Dennis Ritchie, Brian Kerningham"
},
new()
{
Id = 3,
Isbn = "9780134190440",
Titulo = "The Go Programming Language",
Autor = "Brian Kerningham"
}
};
});
app.Run();
public class Livro
{
... Esta nova chamada para MapGet cria um endpoint GET /livros que responde com um array fixo de livros. Vamos utilizar
este array apenas para testarmos a chamada deste endpoint.
Testando o endpoint
Na estrutura de arquivos do seu projeto, deve haver um arquivo chamado Biblioteca/Biblioteca.http. Se não houver, crie-o. Em seguida,
substitua o conteúdo deste arquivo pelo seguinte:
@url = http://localhost:5000
### Obtém uma lista de livros
GET {{url}}/livros
Accept: application/json Em seguida, instale a extensão REST Client no seu VSCode.

Agora, volte em Biblioteca/Biblioteca.http, note que algumas opções adicionais estão sendo exibidas pelo VSCode.

Agora deve aparecer uma opção Send Request acima da linha GET {{url}}/livros. Execute o seu projeto, em seguida volte neste arquivo e clique nesta opção. Se tudo der certo,
uma requisição GET /livros será enviada para sua aplicação, e a resposta será exibida no lado direito da IDE.

Volte para o Program.cs. Vamos criar um POST agora:
// Cria um novo livro.
app.MapPost("/livros", (Livro livro) =>
{
return livro;
}); MapPost cria um novo endpoint que responde ao verbo POST. Tanto no MapGet quanto no MapPost, o segundo argumento passado para o método é uma função anônima (ou função lambda).
Uma função anônima é, basicamente, uma função que não possui nome, e que pode ser referenciada em uma variável, passada como parâmetro para outras funções, e invocada desta forma.
No caso destes métodos de mapeamento de endpoints, esta função anônima passada é a função que será executada quando o endpoint for solicitado. Os argumentos dessa função são as entradas da requisição (parâmetros de URL, corpo da requisição),
e o valor de retorno é a resposta que deve ser devolvida ao cliente.
No caso deste MapPost que acabamos de escrever, note que há um argumento do tipo Livro. Isto indica para o framework que nosso endpoint deve receber dados que podem ser estruturados em um objeto
do tipo Livro. Como estamos apenas testando, nosso endpoint não fará nada além de retornar o mesmo livro que recebemos como entrada.
Execute o projeto (se já estiver executando, reinicie-o), e altere o seu Biblioteca.http:
@url = http://localhost:5000
### Obtém uma lista de livros
GET {{url}}/livros
Accept: application/json
### Cria um novo livro
POST {{url}}/livros
Accept: application/json
Content-Type: application/json
{
"isbn": "9780321741769",
"titulo": "The C# Programming Language",
"autor": "Anders Hejlsberg"
} Adicionamos uma nova linha POST {{url}}/livros. Cada linha abaixo desta representa um cabeçalho, o Accept e o Content-Type.
O Content-Type indica para o servidor o tipo de recurso que estamos enviando. Como estamos enviando um JSON, o Content-Type deve ser application/json (valores de Content-Type são mimetypes).
Logo abaixo dos cabeçalhos, definimos o corpo da requisição que vamos enviar. Cada campo deste JSON é representado por um campo da classe Livro no nosso código.
O framework irá realizar a leitura deste JSON e a sua conversão em um objeto automaticamente.
Agora, experimente executar esta requisição, e observe a saída.

Para criar um PUT, a sintaxe é a mesma, porém utilizando MapPut:
// Edita um livro.
app.MapPut("/livros/{id}", (long id, Livro livro) =>
{
return new { editando = id, dados = livro };
}); A url que definimos agora possui um parâmetro definido por "{id}". Isto significa que este pedaço da
url delimitado por chaves pode assumir qualquer valor numérico, por exemplo, PUT /livros/100, PUT /livros/200, etc.
Reinicie o projeto. Assim como no POST e no GET, altere o Biblioteca.http e teste a execução do endpoint:
### Edita um livro
PUT {{url}}/livros/1
Accept: application/json
Content-Type: application/json
{
"isbn": "9780321741769",
"titulo": "The C# Programming Language",
"autor": "Anders Hejlsberg, Mads Torgensen"
} 
Agora, só falta o GET /livros/{id} e o DELETE /livros/{id}:
// Obtém os dados de um livro individual.
app.MapGet("/livros/{id}", (long id) =>
{
return new Livro() { Id = id };
});
// Remove um livro.
app.MapDelete("/livros/{id}", (long id) =>
{
return Results.NoContent();
}); Reinicie o projeto, altere o Biblioteca.http, e teste estes endpoints:
### Obtém um livro individual
GET {{url}}/livros/2
Accept: application/json
### Remove um livro
DELETE {{url}}/livros/3
Accept: application/json Certifique-se de que você consegue executar todos estes endpoints sem nenhum erro. Nas próximas etapas, iremos fazer a conexão do projeto com um banco de dados para que possamos implementar de fato os endpoints, inserindo, modificando e consultando os dados.
O código fonte do projeto até agora pode ser baixado aqui.