Desafio Técnico · REST API · .NET

Catálogo
& Pedidos

API REST construída em .NET com arquitetura em camadas, domain-driven design, validações no domínio via construtor, e cobertura de testes unitários em Domain + Application.

.NET 9 EF Core InMemory Clean Architecture xUnit + NSubstitute Serilog Scalar / Swagger
Ver Arquitetura → 📊 Coverage Report

// 01 · arquitetura

Clean Architecture
em camadas

O projeto segue os princípios de Clean Architecture, isolando o domínio de infraestrutura e frameworks. A dependência sempre aponta para dentro — o Domain não conhece ninguém.

API
ProdutosController
PedidosController
Scalar / Swagger
AppInitializer
RegisterPipeline
LogExtensions
↓ depende de →
Application
ProdutosService
PedidosService
IProdutosService
IPedidosService
ProdutoRequestDto
PedidoRequestDto
ProdutoDto
PedidoCompletoDto
Presenters / Mappers
DataSeeder
↓ depende de →
Domain
Produto (Entity)
Pedido (Agregado)
ItemPedido (Entity)
Pedido.Status (Value Object)
IProdutosRepository
IPedidosRepository
↓ implementado por →
Infra
AppDbContext
EF Core InMemory
ProdutoRepository
PedidoRepository
ProdutoConfiguration
PedidoConfiguration
ItemPedidoConfiguration
InfraDependencyInjection
Shared
ICommandResult<T>
CommandResult<T>
ToResult() Extension
🧱
Domain Layer
Entidades com validações no construtor. Nenhuma annotation de framework. A regra de negócio vive aqui e em nenhum outro lugar.
⚙️
Application Layer
Orquestra os casos de uso. Services retornam ICommandResult<T> com StatusCode e Message. Zero lógica nos controllers.
🗄️
EF Core InMemory
Banco em memória para a POC. Seed automático com 10 produtos e 5 pedidos em estados variados ao iniciar a aplicação.
📤
CommandResult Pattern
Services retornam ICommandResult com StatusCode HTTP embutido. Extension method .ToResult() converte para IActionResult.
📋
Serilog
Logs estruturados com níveis corretos: Information, Warning e Error. Padrão Service-Método: mensagem em todos os métodos.
📑
Scalar + Swagger
Documentação interativa disponível. Swagger ativo por padrão em Development. Scalar disponível como alternativa.

// 02 · endpoints

10 endpoints,
2 domínios

Produtos e Pedidos cobertos com operações CRUD completas e regras de negócio específicas por domínio.

PRODUTOS · /api/produtos

GET /api/produtos Lista todos os produtos ativos
200
GET /api/produtos/{id} Produto por ID
200404
POST /api/produtos Cria novo produto
201400
PUT /api/produtos/{id} Atualiza produto existente
204400404
DEL /api/produtos/{id} Soft delete — marca como inativo
204404

PEDIDOS · /api/pedidos

GET /api/pedidos Lista todos os pedidos com itens
200204
GET /api/pedidos/{id} Pedido completo com itens e produtos
200404
POST /api/pedidos Cria pedido — valida produtos e agrupa itens
201400404
PUT /api/pedidos/{id}/status Avança status — respeita máquina de estados
204400404
DEL /api/pedidos/{id} Cancela pedido — status → Cancelado
204404

// 03 · máquina de estados

Status flow
de pedidos

Transições de status são controladas por uma máquina de estados no domínio. Transições inválidas lançam InvalidOperationException, retornando 400 Bad Request.

⏳ Pendente
← estado inicial
├──▶
⚙️ Processando
├──▶
📦 Enviado
└──▶
✅ Entregue
└──▶
❌ Cancelado
└──▶
❌ Cancelado
Entregue e Cancelado são estados terminais — nenhuma transição é permitida a partir deles.
A máquina de estados é implementada como um Dictionary<string, HashSet<string>> dentro da classe Pedido.Status, garantindo que a regra viva no domínio e não em validações espalhadas.

DELETE /api/pedidos/{id} internamente chama pedido.CancelarPedido() — não há exclusão física. O registro persiste para auditoria.

// 04 · decisões de design

Por que fizemos
assim?

Escolhas técnicas intencionais que diferenciam esta implementação.

01
Validações no construtor, não em annotations
O domínio é a única fonte de verdade. new Produto("", 0) lança ArgumentException imediatamente — impossível criar um objeto inválido. As annotations ficam apenas nos DTOs de entrada da API.
02
Itens do pedido agrupados como HashSet
Se o cliente envia produtoId: 1, qtde: 2 e produtoId: 1, qtde: 3, o sistema consolida para um único item com quantidade 5. Itens sem diferencial (desconto, presente) não devem se duplicar.
03
Exceptions mapeadas vs. não mapeadas
ArgumentException e InvalidOperationException vêm do domínio com mensagens controladas e são retornadas ao cliente. Exception genérica retorna mensagem genérica — o cliente não precisa saber de erros internos.
04
PrecoUnitario capturado no momento do pedido
O ItemPedido captura produto.Preco no construtor. Se o preço do produto mudar depois, os pedidos existentes não são afetados. Isso garante integridade histórica dos pedidos.
05
ICommandResult pattern
Services retornam ICommandResult<T> com HttpStatusCode, Message e Data. O extension method .ToResult() converte automaticamente para IActionResult, mantendo os controllers em 3 linhas.
06
Soft delete em ambos os domínios
DELETE /produtos/{id} chama produto.Desativar() e persiste. DELETE /pedidos/{id} chama pedido.CancelarPedido(). Nenhum registro é removido do banco — rastreabilidade preservada.

// 05 · testes

Cobertura em
Domain + Application

Testes unitários com xUnit, NSubstitute para mocks e FluentAssertions para asserções legíveis.

🔷 Scalar
Interface moderna para explorar e testar os endpoints. Acesse em /scalar após rodar a API.
# URL padrão
http://localhost:5160/scalar

# Ou via HTTPS
https://localhost:7193/scalar
🟠 Postman
Collection com todos os endpoints e payloads prontos. Importe o arquivo .json da pasta /docs.
# Import no Postman
File → Import → Upload Files
Api.Itau.postman_collection.json
📄 .http file (VS / Rider)
Arquivo Api.Itau.http com todos os cenários incluindo casos de erro. Rode direto no Visual Studio ou JetBrains Rider.
@host = http://localhost:5160

GET {{host}}/api/produtos
Accept: application/json
###
POST {{host}}/api/pedidos
Content-Type: application/json
📊 Coverage Report
Relatório de cobertura de testes gerado pelo projeto. Visualize detalhes por classe e método.
📊 Ver Report →

Cobertura por camada

Domain — Produto~35 testes
Domain — Pedido + ItemPedido~40 testes
Application — ProdutosService~20 testes
Application — PedidosService~20 testes

// 06 · roadmap

O que eu faria
com mais tempo

Esta é uma POC executada em poucas horas. As features abaixo seriam as próximas evoluções naturais.

📄
PaginatedResult para GetAll
Implementaria um PaginatedResult<T> com page, pageSize, totalItems e totalPages para os endpoints GET /produtos e GET /pedidos. Sem paginação, um GetAll em produção é um risco.
Performance
⏱️
Stopwatch por requisição
Adicionaria um Stopwatch nos Services, especialmente no GetAll(), para logar o tempo de execução e identificar gargalos. Especialmente útil com banco real.
Observability
🔐
JWT para autenticação e autorização
Implementaria autenticação via JWT com [Authorize] nos controllers. Diferenciaria roles — por exemplo, apenas admins poderiam criar/deletar produtos.
Segurança
🔑
Idempotency-Key no header
Adicionaria suporte ao header Idempotency-Key no POST /pedidos. Se o cliente reenviar a mesma requisição (retry em falha de rede), o servidor retorna o resultado original sem criar pedido duplicado.
Resiliência
🗃️
Banco de dados real + Migrations
Trocar o UseInMemoryDatabase por SQL Server ou PostgreSQL. O AppDbContext e as EntityConfigurations já estão prontos — é só trocar o provider e rodar dotnet ef migrations add.
Infra
🧪
Testes de integração
Adicionaria testes de integração com WebApplicationFactory<Program> para testar o pipeline completo HTTP → Controller → Service → Repository → EF Core, sem mocks.
Qualidade