Módulo 4 · APIs e sua Arquitetura — Capítulo 08

Projeto Prático — API com Procedures

Fechando o módulo: uma API completa de Pedidos — paginação, autenticação JWT, tratamento de erro e Swagger, tudo sobre as procedures do Módulo 2.

1. O que este projeto amarra

CapítuloPeça aplicada no projeto
01Recursos REST: /api/produtos, /api/pedidos
02Controllers com [ApiController]
03DTOs como record, sem expor entidades
04Login com JWT, endpoints protegidos por role
05Swagger documentando tudo, incluindo o esquema Bearer
06Middleware traduzindo SqlException 50010 em 409
07Paginação na listagem de pedidos

2. O endpoint de checkout

C# PedidoController.cs
[ApiController]
[Route("api/pedidos")]
[Authorize]
public class PedidoController : ControllerBase
{
    private readonly IPedidoRepository _repositorio;
    public PedidoController(IPedidoRepository repositorio) => _repositorio = repositorio;

    [HttpPost("checkout")]
    [ProducesResponseType(typeof(PedidoCriadoDto), StatusCodes.Status201Created)]
    [ProducesResponseType(StatusCodes.Status409Conflict)]
    public async Task<ActionResult<PedidoCriadoDto>> Checkout(CheckoutDto dto)
    {
        int clienteId = int.Parse(User.FindFirstValue(ClaimTypes.NameIdentifier)!);

        int pedidoId = await _repositorio.CheckoutAsync(clienteId, dto.Itens);

        return CreatedAtAction(nameof(Obter), new { id = pedidoId },
            new PedidoCriadoDto(pedidoId));
    }

    [HttpGet("{id:int}")]
    public async Task<ActionResult<PedidoDto>> Obter(int id)
    {
        var pedido = await _repositorio.ObterPorIdAsync(id);
        return pedido is null ? NotFound() : Ok(pedido);
    }

    [HttpGet]
    public async Task<ActionResult<PaginaResultado<PedidoDto>>> Listar(
        [FromQuery] int pagina = 1, [FromQuery] int tamanhoPagina = 20)
    {
        var clienteId = int.Parse(User.FindFirstValue(ClaimTypes.NameIdentifier)!);
        var (itens, total) = await _repositorio.ListarPorClienteAsync(clienteId, pagina, tamanhoPagina);
        return Ok(new PaginaResultado<PedidoDto>(itens, pagina, tamanhoPagina, total));
    }
}

public record CheckoutDto(List<ItemCarrinhoDto> Itens);
public record ItemCarrinhoDto(int ProdutoId, int Quantidade);
public record PedidoCriadoDto(int PedidoId);

User.FindFirstValue(ClaimTypes.NameIdentifier) lê o Id do cliente diretamente do JWT decodificado pelo middleware de autenticação (capítulo 4) — o Controller nunca precisa confiar num clienteId enviado pelo corpo da requisição, o que fecha uma brecha de segurança óbvia (alguém tentando fazer checkout em nome de outro cliente).

Por que isso é mais seguro que receber clienteId no corpo

Se CheckoutDto incluísse um campo ClienteId preenchido pelo próprio cliente da requisição, qualquer usuário autenticado poderia forjar pedidos em nome de outra pessoa só trocando esse valor. Extrair o Id do token assinado (JWT) garante que o pedido é sempre atribuído a quem realmente está autenticado.

3. O middleware de erro, especializado para este projeto

C# mapeamento de erro do domínio de Pedidos
static (int Status, string Mensagem) MapearErro(Exception? excecao) => excecao switch
{
    SqlException { Number: 50010 } => (StatusCodes.Status409Conflict, "Estoque insuficiente para um ou mais itens"),
    ArgumentException                => (StatusCodes.Status400BadRequest, excecao.Message),
    _                                 => (StatusCodes.Status500InternalServerError, "Erro interno"),
};

4. Program.cs completo

C# Program.cs — tudo junto
var builder = WebApplication.CreateBuilder(args);

builder.Services.AddControllers();
builder.Services.AddScoped<IPedidoRepository>(_ =>
    new PedidoRepository(builder.Configuration.GetConnectionString("Default")!));

builder.Services
    .AddAuthentication(JwtBearerDefaults.AuthenticationScheme)
    .AddJwtBearer(opcoes => { /* ...configuração do capítulo 4... */ });
builder.Services.AddAuthorization();

builder.Services.AddEndpointsApiExplorer();
builder.Services.AddSwaggerGen(/* ...configuração do capítulo 5, incluindo Bearer... */);

var app = builder.Build();

app.UseExceptionHandler(appErro => appErro.Run(async context =>
{
    var (status, mensagem) = MapearErro(context.Features.Get<IExceptionHandlerFeature>()?.Error);
    context.Response.StatusCode = status;
    await context.Response.WriteAsJsonAsync(new ProblemDetails { Status = status, Title = mensagem });
}));

if (app.Environment.IsDevelopment())
{
    app.UseSwagger();
    app.UseSwaggerUI();
}

app.UseAuthentication();
app.UseAuthorization();
app.MapControllers();

app.Run();

5. Onde isso te leva

Com uma API REST completa, segura e documentada, os dois módulos finais deste curso saem do universo "requisição/resposta" para dois outros formatos de aplicação C#: serviços que rodam continuamente em segundo plano (Worker Services) e aplicações com interface gráfica própria (Desktop) — ambos ainda se conectando às mesmas procedures do Módulo 2.

📌 Resumo do capítulo

  • Extraia identificadores de segurança (como o Id do usuário) do JWT validado, nunca de um campo enviado pelo cliente na requisição.
  • Um middleware de erro específico do domínio traduz códigos de exceção customizados (como o 50010 do Módulo 2) em respostas HTTP com significado.
  • Uma API de produção combina paginação, autenticação, tratamento de erro e documentação — nenhuma dessas peças isoladamente é suficiente.

✏️ Praticando

  1. Implemente PedidoController completo, incluindo o endpoint de checkout com extração do Id do cliente via JWT.
  2. Teste o fluxo completo: login → obter token → checkout autenticado → consultar o pedido criado.
  3. Force o cenário de estoque insuficiente através da API e confirme o 409 com mensagem clara, documentado no Swagger.