Implementando CRUD Completo em API .NET: Inclusão de Registros com HTTP POST

Neste artigo, vamos explorar a implementação completa do método HTTP POST para inclusão de registros em uma API .NET, complementando as operações CRUD (Create, Read, Update, Delete) que já discutimos anteriormente.

Visão Geral do CRUD Completo

Vamos começar revisando a estrutura completa do controller com todos os métodos CRUD:

[ApiController]
[Route("api/[controller]")]
public class ClientesController : ControllerBase
{
    private readonly IClienteService _clienteService;
    private readonly ILogger<ClientesController> _logger;

    public ClientesController(IClienteService clienteService, ILogger<ClientesController> logger)
    {
        _clienteService = clienteService;
        _logger = logger;
    }

    // GET: api/clientes
    [HttpGet]
    public async Task<IActionResult> GetAll()
    {
        var clientes = await _clienteService.ObterTodosAsync();
        return Ok(clientes);
    }

    // GET api/clientes/5
    [HttpGet("{id}")]
    public async Task<IActionResult> GetById(int id)
    {
        var cliente = await _clienteService.ObterPorIdAsync(id);
        if (cliente == null) return NotFound();
        return Ok(cliente);
    }

    // POST api/clientes
    [HttpPost]
    public async Task<IActionResult> Create([FromBody] Cliente cliente)
    {
        if (!ModelState.IsValid)
            return BadRequest(ModelState);

        var resultado = await _clienteService.CriarAsync(cliente);
        if (!resultado.Sucesso)
            return BadRequest(resultado.Mensagem);

        return CreatedAtAction(nameof(GetById), new { id = cliente.Id }, cliente);
    }

    // PUT api/clientes/5
    [HttpPut("{id}")]
    public async Task<IActionResult> Update(int id, [FromBody] Cliente cliente)
    {
        if (id != cliente.Id)
            return BadRequest("ID inconsistente");

        var resultado = await _clienteService.AtualizarAsync(cliente);
        if (!resultado.Sucesso)
            return BadRequest(resultado.Mensagem);

        return NoContent();
    }

    // DELETE api/clientes/5
    [HttpDelete("{id}")]
    public async Task<IActionResult> Delete(int id)
    {
        var resultado = await _clienteService.ExcluirAsync(id);
        if (!resultado.Sucesso)
            return NotFound(resultado.Mensagem);

        return NoContent();
    }
}

Implementação Detalhada do Método POST

Vamos focar na implementação robusta do método POST para criação de registros:

[HttpPost]
public async Task<IActionResult> Create([FromBody] ClienteCreateDto clienteDto)
{
    try
    {
        // Validação do modelo
        if (!ModelState.IsValid)
        {
            _logger.LogWarning("Tentativa de criação com modelo inválido: {Erros}", 
                ModelState.Values.SelectMany(v => v.Errors));
            return BadRequest(ModelState);
        }

        // Validações de negócio adicionais
        if (await _clienteService.ExistePorEmailAsync(clienteDto.Email))
            return Conflict("Já existe um cliente com este e-mail");

        if (await _clienteService.ExistePorCpfAsync(clienteDto.Cpf))
            return Conflict("Já existe um cliente com este CPF");

        // Mapeamento DTO para entidade
        var cliente = _mapper.Map<Cliente>(clienteDto);

        // Geração de código automático se necessário
        if (string.IsNullOrEmpty(clienteDto.Codigo))
        {
            cliente.Codigo = await _clienteService.GerarCodigoUnicoAsync();
        }

        // Execução da criação
        var resultado = await _clienteService.CriarAsync(cliente);

        if (!resultado.Sucesso)
        {
            _logger.LogError("Falha ao criar cliente: {Erro}", resultado.Mensagem);
            return StatusCode(500, "Erro interno ao processar a solicitação");
        }

        // Retorno com location header
        return CreatedAtAction(
            nameof(GetById), 
            new { id = cliente.Id }, 
            new { 
                Id = cliente.Id, 
                Codigo = cliente.Codigo,
                Mensagem = "Cliente criado com sucesso" 
            });
    }
    catch (Exception ex)
    {
        _logger.LogError(ex, "Erro inesperado ao criar cliente");
        return StatusCode(500, "Erro interno do servidor");
    }
}

DTOs (Data Transfer Objects) para Validação

Utilize DTOs para validação e controle dos dados de entrada:

public class ClienteCreateDto
{
    [Required(ErrorMessage = "O nome é obrigatório")]
    [StringLength(100, MinimumLength = 3, ErrorMessage = "O nome deve ter entre 3 e 100 caracteres")]
    public string Nome { get; set; }

    [Required(ErrorMessage = "O e-mail é obrigatório")]
    [EmailAddress(ErrorMessage = "E-mail em formato inválido")]
    public string Email { get; set; }

    [Required(ErrorMessage = "O CPF é obrigatório")]
    [CpfValidation(ErrorMessage = "CPF inválido")]
    public string Cpf { get; set; }

    [StringLength(11, MinimumLength = 10, ErrorMessage = "Telefone inválido")]
    public string Telefone { get; set; }

    public string Codigo { get; set; } // Opcional - gerado automaticamente se vazio
}

// Validador customizado para CPF
public class CpfValidationAttribute : ValidationAttribute
{
    public override bool IsValid(object value)
    {
        if (value == null || string.IsNullOrEmpty(value.ToString()))
            return false;

        var cpf = value.ToString().Replace(".", "").Replace("-", "");
        return IsCpfValid(cpf);
    }

    private bool IsCpfValid(string cpf)
    {
        // Implementação da validação de CPF
        if (cpf.Length != 11) return false;
        // Lógica de validação do CPF...
        return true;
    }
}

Service Layer com Regras de Negócio

Implemente a camada de serviço para encapsular a lógica de negócio:

public class ClienteService : IClienteService
{
    private readonly IClienteRepository _clienteRepository;
    private readonly ILogger<ClienteService> _logger;

    public ClienteService(IClienteRepository clienteRepository, ILogger<ClienteService> logger)
    {
        _clienteRepository = clienteRepository;
        _logger = logger;
    }

    public async Task<OperacaoResultado> CriarAsync(Cliente cliente)
    {
        try
        {
            // Validações adicionais de negócio
            if (cliente.DataNascimento.HasValue && 
                cliente.DataNascimento.Value > DateTime.Now.AddYears(-18))
            {
                return OperacaoResultado.Falha("Cliente deve ser maior de 18 anos");
            }

            if (await _clienteRepository.ExistePorEmailAsync(cliente.Email))
                return OperacaoResultado.Falha("E-mail já cadastrado");

            // Pré-processamento
            cliente.DataCriacao = DateTime.Now;
            cliente.Ativo = true;

            // Persistência
            await _clienteRepository.AdicionarAsync(cliente);
            await _clienteRepository.SalvarAsync();

            _logger.LogInformation("Cliente {ClienteId} criado com sucesso", cliente.Id);
            return OperacaoResultado.Sucesso();
        }
        catch (Exception ex)
        {
            _logger.LogError(ex, "Erro ao criar cliente");
            return OperacaoResultado.Falha("Erro interno ao criar cliente");
        }
    }

    public async Task<string> GerarCodigoUnicoAsync()
    {
        string codigo;
        do
        {
            codigo = "CLI" + DateTime.Now.ToString("yyyyMMddHHmmssfff");
        } while (await _clienteRepository.ExistePorCodigoAsync(codigo));

        return codigo;
    }

    // Outros métodos do serviço...
}

public class OperacaoResultado
{
    public bool Sucesso { get; set; }
    public string Mensagem { get; set; }
    public object Dados { get; set; }

    public static OperacaoResultado Sucesso(object dados = null) 
        => new OperacaoResultado { Sucesso = true, Dados = dados };

    public static OperacaoResultado Falha(string mensagem) 
        => new OperacaoResultado { Sucesso = false, Mensagem = mensagem };
}

Repository Pattern para Acesso a Dados

Implemente o padrão Repository para abstrair o acesso ao banco de dados:

public interface IClienteRepository
{
    Task<Cliente> ObterPorIdAsync(int id);
    Task<IEnumerable<Cliente>> ObterTodosAsync();
    Task<bool> ExistePorEmailAsync(string email);
    Task<bool> ExistePorCpfAsync(string cpf);
    Task<bool> ExistePorCodigoAsync(string codigo);
    Task AdicionarAsync(Cliente cliente);
    Task AtualizarAsync(Cliente cliente);
    Task RemoverAsync(Cliente cliente);
    Task SalvarAsync();
}

public class ClienteRepository : IClienteRepository
{
    private readonly AppDbContext _context;

    public ClienteRepository(AppDbContext context)
    {
        _context = context;
    }

    public async Task AdicionarAsync(Cliente cliente)
    {
        await _context.Clientes.AddAsync(cliente);
    }

    public async Task<bool> ExistePorEmailAsync(string email)
    {
        return await _context.Clientes
            .AnyAsync(c => c.Email == email);
    }

    public async Task<Cliente> ObterPorIdAsync(int id)
    {
        return await _context.Clientes
            .FirstOrDefaultAsync(c => c.Id == id);
    }

    public async Task SalvarAsync()
    {
        await _context.SaveChangesAsync();
    }

    // Implementação dos outros métodos...
}

Configuração de Injeção de Dependência

Configure a injeção de dependência no Startup.cs ou Program.cs:

// Program.cs
builder.Services.AddScoped<IClienteRepository, ClienteRepository>();
builder.Services.AddScoped<IClienteService, ClienteService>();
builder.Services.AddAutoMapper(typeof(Program));
builder.Services.AddDbContext<AppDbContext>(options =>
    options.UseSqlServer(builder.Configuration.GetConnectionString("DefaultConnection")));

// Configuração do Swagger
builder.Services.AddSwaggerGen(c =>
{
    c.SwaggerDoc("v1", new OpenApiInfo { Title = "Clientes API", Version = "v1" });
});

// Configuração do AutoMapper
builder.Services.AddAutoMapper(typeof(MappingProfile));

public class MappingProfile : Profile
{
    public MappingProfile()
    {
        CreateMap<ClienteCreateDto, Cliente>();
        CreateMap<Cliente, ClienteResponseDto>();
    }
}

Modelo de Entidade Completo

Defina a entidade Cliente com todas as propriedades necessárias:

public class Cliente
{
    public int Id { get; set; }

    [Required]
    [StringLength(20)]
    public string Codigo { get; set; }

    [Required]
    [StringLength(100)]
    public string Nome { get; set; }

    [Required]
    [EmailAddress]
    [StringLength(100)]
    public string Email { get; set; }

    [Required]
    [StringLength(11)]
    public string Cpf { get; set; }

    [StringLength(15)]
    public string Telefone { get; set; }

    public DateTime? DataNascimento { get; set; }

    public DateTime DataCriacao { get; set; }
    public DateTime? DataAtualizacao { get; set; }

    public bool Ativo { get; set; }

    // Navegação
    public virtual ICollection<Pedido> Pedidos { get; set; }

    public Cliente()
    {
        Pedidos = new HashSet<Pedido>();
        DataCriacao = DateTime.Now;
        Ativo = true;
    }
}

Testes Unitários para o Método POST

Implemente testes unitários para garantir a qualidade do código:

[TestFixture]
public class ClientesControllerTests
{
    private Mock<IClienteService> _mockClienteService;
    private ClientesController _controller;

    [SetUp]
    public void Setup()
    {
        _mockClienteService = new Mock<IClienteService>();
        _controller = new ClientesController(_mockClienteService.Object);
    }

    [Test]
    public async Task Create_ClienteValido_RetornaCreated()
    {
        // Arrange
        var clienteDto = new ClienteCreateDto
        {
            Nome = "João Silva",
            Email = "joao@email.com",
            Cpf = "12345678901"
        };

        var cliente = new Cliente { Id = 1, Codigo = "CLI001" };
        _mockClienteService.Setup(s => s.CriarAsync(It.IsAny<Cliente>()))
            .ReturnsAsync(OperacaoResultado.Sucesso(cliente));

        // Act
        var result = await _controller.Create(clienteDto);

        // Assert
        Assert.IsInstanceOf<CreatedAtActionResult>(result);
        _mockClienteService.Verify(s => s.CriarAsync(It.IsAny<Cliente>()), Times.Once);
    }

    [Test]
    public async Task Create_ModeloInvalido_RetornaBadRequest()
    {
        // Arrange
        var clienteDto = new ClienteCreateDto { Nome = "J" }; // Nome muito curto
        _controller.ModelState.AddModelError("Nome", "Nome muito curto");

        // Act
        var result = await _controller.Create(clienteDto);

        // Assert
        Assert.IsInstanceOf<BadRequestObjectResult>(result);
    }
}

Melhores Práticas e Considerações

Algumas práticas recomendadas para implementação de APIs RESTful:

  • Validação em Múltiplas Camadas: Valide tanto no DTO quanto na camada de serviço
  • Tratamento de Exceções: Use middleware global para tratamento de erros
  • Logging Abrangente: Registre todas as operações importantes
  • Versionamento: Implemente versionamento da API desde o início
  • Documentação: Use Swagger/OpenAPI para documentação automática
  • Performance: Considere operações assíncronas para melhor escalabilidade
  • Segurança: Implemente autenticação e autorização adequadas

Exemplo de Request/Response

Request:

POST /api/clientes
Content-Type: application/json

{
  "nome": "Maria Santos",
  "email": "maria@email.com",
  "cpf": "98765432100",
  "telefone": "11999998888",
  "dataNascimento": "1990-01-15"
}

Response (201 Created):

{
  "id": 1,
  "codigo": "CLI20231015123456",
  "mensagem": "Cliente criado com sucesso"
}

Conclusão

A implementação do método HTTP POST completa o ciclo CRUD essencial para qualquer API RESTful. Seguindo as práticas recomendadas de validação, tratamento de erros, separação de concerns e documentação, você estará construindo APIs robustas, escaláveis e maintainable.

Lembre-se: Uma API bem projetada não apenas funciona corretamente, mas também é intuitiva para consumir, bem documentada, segura e preparada para evoluir com as necessidades do negócio. Invista tempo no design da API e na implementação de boas práticas desde o início.

Nos próximos artigos, exploraremos tópicos avançados como paginação, filtros complexos, caching, e implementação de padrões como CQRS e MediaTR.