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.