Tópicos que você verá nesse post
- O que é uma API
- Do console para a web
- Configurando a conexão
- Formando o JSON
- Criando os endpoints
- Testando a API
Contexto
No post anterior, conectamos o backend ao banco de dados da loja e mostramos as categorias e os produtos no console. O backend já sabe buscar as informações, mas elas só aparecem para quem está executando o projeto.
Para que a tela da loja consiga exibir esses dados, o frontend precisa de uma forma de pedir essas informações para o backend. E é aqui que entra a API.
O que é uma API
Uma API (Application Programming Interface) é a interface que o backend disponibiliza para que outras aplicações consigam conversar com ele. No caso da web, essa conversa acontece através do protocolo HTTP: o frontend envia uma requisição para um endereço e o backend devolve uma resposta.
O frontend conversa com a API, que utiliza o repositório para buscar os dados no banco
Cada requisição possui alguns elementos:
- Endereço: o caminho que identifica o recurso, como
/api/products; - Método: a ação que queremos realizar. O
GETbusca informações, oPOSTcria, oPUTatualiza e oDELETEremove. Repare que são equivalentes aos comandosSELECT,INSERT,UPDATEeDELETEque vimos na série de banco de dados; - Parâmetros: informações extras, como o filtro de uma categoria em
/api/products?category=Camisetas.
E cada resposta possui um status code, um número que indica o resultado da requisição. Entre os mais comuns:
- 200 OK: a requisição deu certo;
- 201 Created: um registro foi criado;
- 400 Bad Request: a requisição enviada possui algum problema;
- 404 Not Found: o recurso solicitado não foi encontrado;
- 500 Internal Server Error: aconteceu um erro inesperado no backend.
Do console para a web
Ao invés de criar um projeto novo, vamos aproveitar o que já construímos: as models, o contexto e os repositórios continuam exatamente os mesmos. O que muda é como essas informações são entregues.
No arquivo Shopping.csproj, altere o SDK do projeto de Microsoft.NET.Sdk para Microsoft.NET.Sdk.Web e remova a linha do OutputType:
<Project Sdk="Microsoft.NET.Sdk.Web">
<PropertyGroup>
<TargetFramework>net10.0</TargetFramework>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Microsoft.EntityFrameworkCore.SqlServer" Version="10.0.13" />
</ItemGroup>
</Project>
O SDK web traz o ASP.NET Core, o framework do .NET para a construção de aplicações web e APIs. Ao final do post, a estrutura do projeto ficará assim:
Shopping/
├── Data/
│ └── ShoppingContext.cs
├── Models/
│ ├── Category.cs
│ └── Product.cs
├── Repositories/
│ ├── CategoryRepository.cs
│ └── ProductRepository.cs
├── Responses/
│ ├── CategoryResponse.cs
│ └── ProductResponse.cs
├── appsettings.json
├── Program.cs
└── Shopping.csproj
Configurando a conexão
Como comentamos no post anterior, a string de conexão não deveria ficar no código. Crie um arquivo appsettings.json na raiz do projeto:
{
"ConnectionStrings": {
"Shopping": "Server=localhost,1433;Database=Shopping;User Id=sa;Password=Sa#p4ssword;TrustServerCertificate=True"
}
}
E o contexto deixa de configurar a conexão sozinho para recebê-la de fora, através do construtor:
using Microsoft.EntityFrameworkCore;
using Shopping.Models;
namespace Shopping.Data;
public class ShoppingContext : DbContext
{
public ShoppingContext(DbContextOptions<ShoppingContext> options) : base(options)
{
}
public DbSet<Category> Categories => Set<Category>();
public DbSet<Product> Products => Set<Product>();
}
Quem vai entregar essa configuração é o próprio ASP.NET Core, como veremos no Program.cs. Em um ambiente real, a senha ainda sairia desse arquivo e ficaria em variáveis de ambiente ou em um cofre de segredos, mas para os nossos exemplos o appsettings.json é suficiente.
Formando o JSON
Antes de criar os endereços da API, precisamos decidir o que será devolvido para o frontend. Poderíamos devolver as models diretamente, mas elas representam a estrutura do banco de dados e não necessariamente o que a tela precisa. Alguns problemas que isso pode trazer:
- Exposição: qualquer coluna nova na tabela passaria a ser enviada automaticamente, inclusive informações que não deveriam ser públicas;
- Acoplamento: uma mudança no banco de dados mudaria o formato da resposta e quebraria o frontend;
- Relacionamentos: o produto possui uma categoria, que possui uma lista de produtos, que possuem uma categoria... e assim por diante.
Por isso, vamos criar objetos de resposta que definem exatamente o formato do JSON. Em C#, um record é uma ótima opção para isso:
using Shopping.Models;
namespace Shopping.Responses;
public record CategoryResponse(int Id, string Title)
{
public static CategoryResponse From(Category category)
{
return new CategoryResponse(category.Id, category.Title);
}
}
using Shopping.Models;
namespace Shopping.Responses;
public record ProductResponse(
int Id,
string Name,
string? Description,
decimal Price,
string Category)
{
public static ProductResponse From(Product product)
{
return new ProductResponse(
product.Id,
product.Name ?? string.Empty,
product.Description,
product.Price ?? 0,
product.Category?.Title ?? string.Empty);
}
}
Repare em algumas decisões que tomamos aqui:
- Categoria: ao invés do objeto inteiro da categoria, o produto devolve somente o seu título, que é o que a tela exibe;
- Nulos: o nome e o preço deixam de ser nulos na resposta. Se o banco de dados não tiver essa informação, devolvemos um valor padrão e o frontend não precisa tratar esse caso;
- Método
From: centraliza a conversão de uma model para a resposta, então ela não se repete em cada endereço da API.
Esse é o contrato que comentamos no post de frontend: o formato que o backend se compromete a devolver.
Criando os endpoints
Um endpoint é a combinação de um método e um endereço, como o GET /api/products. Com o ASP.NET Core, podemos criá-los diretamente no Program.cs:
using Microsoft.EntityFrameworkCore;
using Shopping.Data;
using Shopping.Repositories;
using Shopping.Responses;
var builder = WebApplication.CreateBuilder(args);
builder.Services.AddDbContext<ShoppingContext>(options =>
options.UseSqlServer(builder.Configuration.GetConnectionString("Shopping")));
builder.Services.AddScoped<CategoryRepository>();
builder.Services.AddScoped<ProductRepository>();
var app = builder.Build();
app.MapGet("/api/categories", (CategoryRepository repository) =>
{
return repository.GetAll().Select(CategoryResponse.From);
});
app.MapGet("/api/products", (string? category, ProductRepository repository) =>
{
var products = category is null
? repository.GetAll()
: repository.GetByCategory(category);
return products.Select(ProductResponse.From);
});
app.MapGet("/api/products/{id}", (int id, ProductRepository repository) =>
{
var product = repository.GetById(id);
return product is null
? Results.NotFound()
: Results.Ok(ProductResponse.From(product));
});
app.Run();
Vamos por partes:
- Serviços: o
AddDbContextregistra o contexto utilizando a string de conexão doappsettings.json, e oAddScopedregistra os repositórios. Antes, no console, nós mesmos criávamos esses objetos comnew. Agora o ASP.NET Core cria e entrega cada um deles para quem precisar, o que chamamos de injeção de dependência; - Categorias: o
GET /api/categoriesdevolve todas as categorias; - Produtos: o
GET /api/productsdevolve todos os produtos ou, caso o parâmetrocategoryseja informado, somente os produtos daquela categoria; - Produto: o
GET /api/products/{id}devolve um produto específico, ou o status 404 caso ele não exista.
Para o último endpoint, adicione o método GetById no ProductRepository:
public Product? GetById(int id)
{
return _context.Products
.Include(product => product.Category)
.FirstOrDefault(product => product.Id == id);
}
Repare que não precisamos converter nada para JSON manualmente: o ASP.NET Core faz essa conversão automaticamente a partir dos objetos de resposta, e ainda transforma os nomes das propriedades para o padrão camelCase, como Name para name.
Testando a API
Execute o projeto:
dotnet run
O endereço em que a API está rodando é exibido no terminal, como http://localhost:5000 (a porta pode ser diferente na sua máquina). Você pode acessar os endpoints diretamente no navegador ou utilizar uma ferramenta como o Postman ou o curl:
curl http://localhost:5000/api/categories
[
{ "id": 3, "title": "Blusas" },
{ "id": 1, "title": "Camisetas" },
{ "id": 4, "title": "Jaquetas" },
{ "id": 2, "title": "Tênis" }
]
Filtrando os produtos pela categoria de tênis:
curl -G http://localhost:5000/api/products --data-urlencode "category=Tênis"
Uma URL não aceita caracteres como o ê diretamente, eles precisam ser codificados (T%C3%AAnis). O navegador faz isso automaticamente, mas no curl utilizamos o --data-urlencode. Caso contrário, a API responde com o status 400 Bad Request.
[
{
"id": 5,
"name": "Tênis A",
"description": "Tênis de corrida",
"price": 549.00,
"category": "Tênis"
},
{
"id": 6,
"name": "Tênis B",
"description": "Tênis casual",
"price": 670.00,
"category": "Tênis"
}
]
E buscando um produto que não existe, a resposta é o status 404 Not Found:
curl -i http://localhost:5000/api/products/99
HTTP/1.1 404 Not Found
Compare com o resultado do post anterior: são as mesmas informações que mostramos no console, mas agora em um formato que qualquer aplicação consegue consumir. No próximo post, vamos fazer o frontend da loja buscar esses dados e construir a tela.
