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.