Implementando Respostas Robustas em APIs .NET: Guia Completo

Neste artigo, vamos explorar estratégias avançadas para implementar respostas robustas e padronizadas em APIs .NET, abordando desde a estrutura básica até padrões avançados de resposta HTTP.

Por Que Padronizar Respostas?

Padronizar as respostas da API é crucial para:

  • Consistência na comunicação entre frontend e backend
  • Melhor experiência de desenvolvimento para consumidores da API
  • Facilidade de debug e tratamento de erros
  • Manutenibilidade do código
  • Compatibilidade com diferentes clientes (web, mobile, desktop)

Criando uma Classe de Resposta Padronizada

Vamos começar criando uma classe base para respostas padronizadas:

public class ApiResponse<T>
{
    public bool Success { get; set; }
    public string Message { get; set; }
    public string ErrorMessage { get; set; }
    public T Data { get; set; }
    public DateTime Timestamp { get; set; }
    public string RequestId { get; set; }

    public ApiResponse()
    {
        Timestamp = DateTime.UtcNow;
        RequestId = Guid.NewGuid().ToString();
    }

    // Métodos estáticos para criação rápida
    public static ApiResponse<T> SuccessResponse(T data, string message = "Operação realizada com sucesso")
    {
        return new ApiResponse<T>
        {
            Success = true,
            Message = message,
            Data = data,
            ErrorMessage = null
        };
    }

    public static ApiResponse<T> ErrorResponse(string errorMessage, string message = "Erro na operação")
    {
        return new ApiResponse<T>
        {
            Success = false,
            Message = message,
            ErrorMessage = errorMessage,
            Data = default
        };
    }
}

// Classe para respostas sem dados
public class ApiResponse
{
    public bool Success { get; set; }
    public string Message { get; set; }
    public string ErrorMessage { get; set; }
    public DateTime Timestamp { get; set; }
    public string RequestId { get; set; }

    public ApiResponse()
    {
        Timestamp = DateTime.UtcNow;
        RequestId = Guid.NewGuid().ToString();
    }

    public static ApiResponse SuccessResponse(string message = "Operação realizada com sucesso")
    {
        return new ApiResponse
        {
            Success = true,
            Message = message,
            ErrorMessage = null
        };
    }

    public static ApiResponse ErrorResponse(string errorMessage, string message = "Erro na operação")
    {
        return new ApiResponse
        {
            Success = false,
            Message = message,
            ErrorMessage = errorMessage
        };
    }
}

Implementando Controladores com Respostas Padronizadas

Agora vamos refatorar nossos controladores para usar as respostas padronizadas:

[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<ActionResult<ApiResponse<IEnumerable<ClienteDto>>>> GetAll()
    {
        try
        {
            var clientes = await _clienteService.ObterTodosAsync();
            var response = ApiResponse<IEnumerable<ClienteDto>>.SuccessResponse(clientes, "Clientes recuperados com sucesso");
            return Ok(response);
        }
        catch (Exception ex)
        {
            _logger.LogError(ex, "Erro ao obter clientes");
            var response = ApiResponse<IEnumerable<ClienteDto>>.ErrorResponse(
                "Erro interno ao processar a solicitação", 
                "Falha ao recuperar clientes"
            );
            return StatusCode(500, response);
        }
    }

    // GET api/clientes/5
    [HttpGet("{id}")]
    public async Task<ActionResult<ApiResponse<ClienteDto>>> GetById(int id)
    {
        try
        {
            var cliente = await _clienteService.ObterPorIdAsync(id);

            if (cliente == null)
            {
                var notFoundResponse = ApiResponse<ClienteDto>.ErrorResponse(
                    $"Cliente com ID {id} não encontrado",
                    "Recurso não encontrado"
                );
                return NotFound(notFoundResponse);
            }

            var successResponse = ApiResponse<ClienteDto>.SuccessResponse(cliente);
            return Ok(successResponse);
        }
        catch (Exception ex)
        {
            _logger.LogError(ex, "Erro ao obter cliente por ID: {Id}", id);
            var response = ApiResponse<ClienteDto>.ErrorResponse(
                "Erro interno ao processar a solicitação",
                "Falha ao recuperar cliente"
            );
            return StatusCode(500, response);
        }
    }

    // POST api/clientes
    [HttpPost]
    public async Task<ActionResult<ApiResponse<ClienteDto>>> Create([FromBody] ClienteCreateDto clienteDto)
    {
        try
        {
            if (!ModelState.IsValid)
            {
                var errors = ModelState.Values
                    .SelectMany(v => v.Errors)
                    .Select(e => e.ErrorMessage)
                    .ToList();

                var validationResponse = ApiResponse<ClienteDto>.ErrorResponse(
                    string.Join("; ", errors),
                    "Dados de entrada inválidos"
                );
                return BadRequest(validationResponse);
            }

            var cliente = await _clienteService.CriarAsync(clienteDto);

            var response = ApiResponse<ClienteDto>.SuccessResponse(
                cliente, 
                "Cliente criado com sucesso"
            );

            return CreatedAtAction(nameof(GetById), new { id = cliente.Id }, response);
        }
        catch (Exception ex)
        {
            _logger.LogError(ex, "Erro ao criar cliente");
            var response = ApiResponse<ClienteDto>.ErrorResponse(
                "Erro interno ao processar a solicitação",
                "Falha ao criar cliente"
            );
            return StatusCode(500, response);
        }
    }

    // DELETE api/clientes/5
    [HttpDelete("{id}")]
    public async Task<ActionResult<ApiResponse>> Delete(int id)
    {
        try
        {
            var resultado = await _clienteService.ExcluirAsync(id);

            if (!resultado.Success)
            {
                var errorResponse = ApiResponse.ErrorResponse(
                    resultado.ErrorMessage,
                    "Falha ao excluir cliente"
                );
                return BadRequest(errorResponse);
            }

            var successResponse = ApiResponse.SuccessResponse("Cliente excluído com sucesso");
            return Ok(successResponse);
        }
        catch (Exception ex)
        {
            _logger.LogError(ex, "Erro ao excluir cliente: {Id}", id);
            var response = ApiResponse.ErrorResponse(
                "Erro interno ao processar a solicitação",
                "Falha ao excluir cliente"
            );
            return StatusCode(500, response);
        }
    }
}

Middleware para Tratamento Global de Exceções

Implemente um middleware para tratamento global de exceções:

public class ExceptionHandlingMiddleware
{
    private readonly RequestDelegate _next;
    private readonly ILogger<ExceptionHandlingMiddleware> _logger;

    public ExceptionHandlingMiddleware(RequestDelegate next, ILogger<ExceptionHandlingMiddleware> logger)
    {
        _next = next;
        _logger = logger;
    }

    public async Task InvokeAsync(HttpContext context)
    {
        try
        {
            await _next(context);
        }
        catch (Exception ex)
        {
            _logger.LogError(ex, "Exceção não tratada capturada pelo middleware");
            await HandleExceptionAsync(context, ex);
        }
    }

    private static Task HandleExceptionAsync(HttpContext context, Exception exception)
    {
        var statusCode = exception switch
        {
            NotFoundException => StatusCodes.Status404NotFound,
            ValidationException => StatusCodes.Status400BadRequest,
            UnauthorizedAccessException => StatusCodes.Status401Unauthorized,
            _ => StatusCodes.Status500InternalServerError
        };

        var response = new ApiResponse
        {
            Success = false,
            Message = "Erro interno do servidor",
            ErrorMessage = exception.Message
        };

        context.Response.ContentType = "application/json";
        context.Response.StatusCode = statusCode;

        return context.Response.WriteAsync(JsonSerializer.Serialize(response));
    }
}

// Extensão para registrar o middleware
public static class ExceptionHandlingMiddlewareExtensions
{
    public static IApplicationBuilder UseExceptionHandling(this IApplicationBuilder app)
    {
        return app.UseMiddleware<ExceptionHandlingMiddleware>();
    }
}

Configuração no Program.cs

Configure o middleware e serviços necessários:

var builder = WebApplication.CreateBuilder(args);

// Add services to the container
builder.Services.AddControllers();
builder.Services.AddEndpointsApiExplorer();
builder.Services.AddSwaggerGen();

// Registrar serviços da aplicação
builder.Services.AddScoped<IClienteService, ClienteService>();
builder.Services.AddScoped<IClienteRepository, ClienteRepository>();

// Configurar logging
builder.Services.AddLogging(logging =>
{
    logging.AddConsole();
    logging.AddDebug();
});

var app = builder.Build();

// Configure the HTTP request pipeline
if (app.Environment.IsDevelopment())
{
    app.UseSwagger();
    app.UseSwaggerUI();
}

app.UseExceptionHandling(); // Middleware global de exceções
app.UseHttpsRedirection();
app.UseAuthorization();
app.MapControllers();

app.Run();

Exceções Personalizadas

Crie exceções personalizadas para diferentes cenários:

public class NotFoundException : Exception
{
    public NotFoundException(string message) : base(message) { }

    public NotFoundException(string name, object key)
        : base($"Entidade \"{name}\" ({key}) não foi encontrada.") { }
}

public class ValidationException : Exception
{
    public IEnumerable<ValidationError> Errors { get; }

    public ValidationException(IEnumerable<ValidationError> errors)
        : base("Erro de validação ocorreu")
    {
        Errors = errors;
    }
}

public class ValidationError
{
    public string PropertyName { get; set; }
    public string ErrorMessage { get; set; }
    public object AttemptedValue { get; set; }
}

public class BusinessException : Exception
{
    public string ErrorCode { get; }

    public BusinessException(string message, string errorCode = "BUSINESS_ERROR") 
        : base(message)
    {
        ErrorCode = errorCode;
    }
}

DTOs para Transferência de Dados

Implemente DTOs para transferência segura de dados:

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

    [Required]
    [StringLength(100, MinimumLength = 2)]
    public string Nome { get; set; }

    [EmailAddress]
    public string Email { get; set; }

    public DateTime DataCriacao { get; set; }
    public bool Ativo { get; set; }
}

public class ClienteCreateDto
{
    [Required]
    [StringLength(100, MinimumLength = 2)]
    public string Nome { get; set; }

    [Required]
    [EmailAddress]
    public string Email { get; set; }

    [StringLength(200)]
    public string Endereco { get; set; }
}

public class ClienteUpdateDto
{
    [Required]
    public int Id { get; set; }

    [StringLength(100, MinimumLength = 2)]
    public string Nome { get; set; }

    [EmailAddress]
    public string Email { get; set; }
}

Service Layer com Tratamento de Erros

Implemente a camada de serviço com tratamento adequado de erros:

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

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

    public async Task<IEnumerable<ClienteDto>> ObterTodosAsync()
    {
        try
        {
            var clientes = await _clienteRepository.ObterTodosAsync();
            return _mapper.Map<IEnumerable<ClienteDto>>(clientes);
        }
        catch (Exception ex)
        {
            _logger.LogError(ex, "Erro ao obter todos os clientes");
            throw new BusinessException("Falha ao recuperar clientes", "CLIENTES_GET_ALL_ERROR");
        }
    }

    public async Task<ClienteDto> ObterPorIdAsync(int id)
    {
        try
        {
            var cliente = await _clienteRepository.ObterPorIdAsync(id);

            if (cliente == null)
            {
                throw new NotFoundException(nameof(Cliente), id);
            }

            return _mapper.Map<ClienteDto>(cliente);
        }
        catch (NotFoundException)
        {
            throw;
        }
        catch (Exception ex)
        {
            _logger.LogError(ex, "Erro ao obter cliente por ID: {Id}", id);
            throw new BusinessException($"Falha ao recuperar cliente {id}", "CLIENTE_GET_ERROR");
        }
    }

    public async Task<ClienteDto> CriarAsync(ClienteCreateDto clienteDto)
    {
        try
        {
            // Validações de negócio
            if (await _clienteRepository.ExistePorEmailAsync(clienteDto.Email))
            {
                throw new BusinessException("Já existe um cliente com este e-mail", "CLIENTE_EMAIL_DUPLICADO");
            }

            var cliente = _mapper.Map<Cliente>(clienteDto);
            cliente.DataCriacao = DateTime.UtcNow;
            cliente.Ativo = true;

            await _clienteRepository.AdicionarAsync(cliente);
            await _clienteRepository.SalvarAsync();

            return _mapper.Map<ClienteDto>(cliente);
        }
        catch (BusinessException)
        {
            throw;
        }
        catch (Exception ex)
        {
            _logger.LogError(ex, "Erro ao criar cliente");
            throw new BusinessException("Falha ao criar cliente", "CLIENTE_CREATE_ERROR");
        }
    }

    public async Task<OperacaoResultado> ExcluirAsync(int id)
    {
        try
        {
            var cliente = await _clienteRepository.ObterPorIdAsync(id);

            if (cliente == null)
            {
                return OperacaoResultado.Falha("Cliente não encontrado");
            }

            // Verificar dependências
            if (await _clienteRepository.TemPedidosAtivosAsync(id))
            {
                return OperacaoResultado.Falha("Não é possível excluir cliente com pedidos ativos");
            }

            await _clienteRepository.RemoverAsync(cliente);
            await _clienteRepository.SalvarAsync();

            return OperacaoResultado.Sucesso();
        }
        catch (Exception ex)
        {
            _logger.LogError(ex, "Erro ao excluir cliente: {Id}", id);
            return OperacaoResultado.Falha("Erro interno ao excluir cliente");
        }
    }
}

public class OperacaoResultado
{
    public bool Success { get; set; }
    public string ErrorMessage { get; set; }

    public static OperacaoResultado Sucesso() => new() { Success = true };
    public static OperacaoResultado Falha(string errorMessage) => new() { Success = false, ErrorMessage = errorMessage };
}

Exemplos de Respostas da API

Resposta de Sucesso (GET /api/clientes/1):

{
  "success": true,
  "message": "Operação realizada com sucesso",
  "errorMessage": null,
  "data": {
    "id": 1,
    "nome": "João Silva",
    "email": "joao@email.com",
    "dataCriacao": "2024-01-15T10:30:00Z",
    "ativo": true
  },
  "timestamp": "2024-01-15T14:25:30.1234567Z",
  "requestId": "a1b2c3d4-e5f6-7890-abcd-ef1234567890"
}

Resposta de Erro (GET /api/clientes/999):

{
  "success": false,
  "message": "Recurso não encontrado",
  "errorMessage": "Entidade \"Cliente\" (999) não foi encontrada.",
  "data": null,
  "timestamp": "2024-01-15T14:26:15.9876543Z",
  "requestId": "b2c3d4e5-f6g7-8901-bcde-f23456789012"
}

Melhores Práticas e Considerações Finais

Algumas práticas recomendadas para respostas de API:

  • Consistência: Mantenha o mesmo formato para todas as respostas
  • Status Codes Appropriados: Use códigos HTTP semanticamente corretos
  • Logging: Registre erros e informações importantes
  • Documentação: Documente os formatos de resposta usando Swagger/OpenAPI
  • Versionamento: Considere versionar sua API para compatibilidade
  • Performance: Use paginação para listas grandes
  • Segurança: Não exponha dados sensíveis ou informações de stack trace

Dica profissional: Considere implementar um middleware de logging que registre todas as requisições e respostas (com dados sensíveis ofuscados) para facilitar o debug em produção. Isso ajuda significativamente na identificação e resolução de problemas.

Com estas implementações, sua API .NET terá respostas padronizadas, robustas e fáceis de consumir, proporcionando uma melhor experiência para os desenvolvedores que integram com seu sistema.