API Response Standardization
Quando Usar
- Padronizar respostas de sucesso e erro da API em um formato único
- Implementar envelope de resposta (
success, data/error, timestamp)
- Criar filtro global para encapsular 200/201 automaticamente
- Tratar exceções globalmente e retornar erros em formato consistente
- Palavras-chave: "padronizar resposta", "envelope API", "formato de erro", "ApiResponse", "GlobalException"
Contrato JSON
Sucesso (200/201)
json
1{
2 "success": true,
3 "data": { ... },
4 "timestamp": "2026-02-21T20:14:35.487525Z"
5}
success: sempre true
data: payload retornado pelo endpoint (objeto ou array)
timestamp: ISO 8601 UTC
Erro (4xx/5xx)
json
1{
2 "success": false,
3 "error": {
4 "code": "Unauthorized",
5 "message": "Acesso não autorizado."
6 },
7 "timestamp": "2026-02-21T20:14:26.9071028Z"
8}
success: sempre false
error.code: identificador do erro (ex.: Unauthorized, InvalidCredentials, BadRequest)
error.message: mensagem descritiva para o cliente
timestamp: ISO 8601 UTC
Implementação
1. Modelos (Api/Models)
ApiResponse.cs — resposta de sucesso:
csharp
1namespace YourApi.Models;
2
3public class ApiResponse<T>
4{
5 public bool Success { get; init; } = true;
6 public T? Data { get; init; }
7 public DateTime Timestamp { get; init; } = DateTime.UtcNow;
8
9 public static ApiResponse<T> CreateSuccess(T data) => new()
10 {
11 Success = true,
12 Data = data,
13 Timestamp = DateTime.UtcNow
14 };
15}
ApiErrorResponse.cs — resposta de erro:
csharp
1namespace YourApi.Models;
2
3public class ApiErrorResponse
4{
5 public bool Success { get; init; } = false;
6 public ErrorDetail Error { get; init; } = null!;
7 public DateTime Timestamp { get; init; } = DateTime.UtcNow;
8
9 public static ApiErrorResponse Create(string code, string message) => new()
10 {
11 Success = false,
12 Error = new ErrorDetail { Code = code, Message = message },
13 Timestamp = DateTime.UtcNow
14 };
15}
16
17public class ErrorDetail
18{
19 public string Code { get; init; } = string.Empty;
20 public string Message { get; init; } = string.Empty;
21}
2. Filtro de sucesso (Api/Filters)
ApiResponseFilter.cs — encapsula automaticamente 200/201 em ApiResponse<T>:
csharp
1using Microsoft.AspNetCore.Mvc;
2using Microsoft.AspNetCore.Mvc.Filters;
3using YourApi.Models;
4
5public class ApiResponseFilter : IActionFilter
6{
7 private readonly ILogger<ApiResponseFilter> _logger;
8
9 public ApiResponseFilter(ILogger<ApiResponseFilter> logger) => _logger = logger;
10
11 public void OnActionExecuting(ActionExecutingContext context) { }
12
13 public void OnActionExecuted(ActionExecutedContext context)
14 {
15 if (context.Result is OkObjectResult okResult)
16 {
17 context.Result = new ObjectResult(ApiResponse<object>.CreateSuccess(okResult.Value!))
18 { StatusCode = StatusCodes.Status200OK };
19 return;
20 }
21 if (context.Result is ObjectResult objectResult &&
22 (objectResult.StatusCode == StatusCodes.Status200OK || objectResult.StatusCode == StatusCodes.Status201Created))
23 {
24 context.Result = new ObjectResult(ApiResponse<object>.CreateSuccess(objectResult.Value!))
25 { StatusCode = objectResult.StatusCode };
26 }
27 }
28}
Controllers continuam retornando Ok(model) ou CreatedAtAction(..., model); o filtro envolve em { "success": true, "data": model, "timestamp": "..." }.
3. Middleware de exceção (Api/Middleware)
GlobalExceptionMiddleware.cs — captura exceções, mapeia para (statusCode, code, message) e retorna ApiErrorResponse:
csharp
1using System.Text.Json;
2using Microsoft.AspNetCore.Http;
3using YourApi.Models;
4
5public class GlobalExceptionMiddleware
6{
7 private readonly RequestDelegate _next;
8 private readonly ILogger<GlobalExceptionMiddleware> _logger;
9
10 public GlobalExceptionMiddleware(RequestDelegate next, ILogger<GlobalExceptionMiddleware> logger)
11 {
12 _next = next;
13 _logger = logger;
14 }
15
16 public async Task InvokeAsync(HttpContext context)
17 {
18 try
19 {
20 await _next(context);
21 }
22 catch (Exception ex)
23 {
24 _logger.LogError(ex, "Unhandled exception: {ExceptionType}", ex.GetType().Name);
25 await HandleExceptionAsync(context, ex);
26 }
27 }
28
29 private static async Task HandleExceptionAsync(HttpContext context, Exception exception)
30 {
31 var (statusCode, code, message) = MapException(exception);
32 context.Response.StatusCode = statusCode;
33 context.Response.ContentType = "application/json";
34
35 var errorResponse = ApiErrorResponse.Create(code, message);
36 var options = new JsonSerializerOptions { PropertyNamingPolicy = JsonNamingPolicy.CamelCase };
37 await context.Response.WriteAsync(JsonSerializer.Serialize(errorResponse, options));
38 }
39
40 private static (int statusCode, string code, string message) MapException(Exception exception)
41 {
42 return exception switch
43 {
44 UnauthorizedAccessException => (StatusCodes.Status401Unauthorized, "Unauthorized", "Acesso não autorizado."),
45 ArgumentException => (StatusCodes.Status400BadRequest, "BadRequest", "Requisição inválida."),
46 KeyNotFoundException => (StatusCodes.Status404NotFound, "NotFound", "Recurso não encontrado."),
47 _ => (StatusCodes.Status500InternalServerError, "InternalServerError", "Erro interno do servidor.")
48 };
49 }
50}
Importante: estender MapException com os tipos de exceção do domínio ou de SDKs (ex.: Cognito, EF Core). Sempre retornar tripla (statusCode, code, message) e usar ApiErrorResponse.Create(code, message).
4. Registro (Program.cs)
csharp
1// Filtro global de resposta (sucesso)
2builder.Services.AddControllers(options =>
3{
4 options.Filters.Add<ApiResponseFilter>();
5});
6
7// JSON: camelCase e timestamp consistente
8builder.Services.AddControllers()
9 .AddJsonOptions(options =>
10 {
11 options.JsonSerializerOptions.PropertyNamingPolicy = JsonNamingPolicy.CamelCase;
12 options.JsonSerializerOptions.DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull;
13 });
14
15// Middleware de exceção (deve estar cedo no pipeline)
16var app = builder.Build();
17app.UseMiddleware<GlobalExceptionMiddleware>();
18// ... UseRouting, UseAuthorization, MapControllers
Princípios
- Sucesso: controllers retornam apenas o payload; o filtro adiciona
success, data e timestamp.
- Erro: exceções são tratadas no middleware; nunca retornar corpo de erro manualmente nos controllers para erros genéricos (401, 404, 500).
- Códigos de erro: usar códigos estáveis (ex.:
Unauthorized, InvalidCredentials) para que clientes possam tratar por error.code; message pode ser localizada ou mais amigável.
- Timestamp: sempre UTC (ISO 8601) para auditoria e debugging.
Exclusões opcionais
- Endpoints como
/health que precisam de corpo específico: no filtro, não encapsular quando context.ActionDescriptor.RouteValues["action"] == "Health" (ou excluir por atributo/controller).
- Respostas que já são
ProblemDetails ou outro contrato: o filtro só atua em OkObjectResult e ObjectResult com 200/201; demais resultados não são alterados.
Checklist de adoção em novo projeto
- Criar
ApiResponse<T>, ApiErrorResponse e ErrorDetail em Api/Models.
- Criar
ApiResponseFilter em Api/Filters e registrar em AddControllers(options => options.Filters.Add<ApiResponseFilter>()).
- Criar
GlobalExceptionMiddleware em Api/Middleware, implementar MapException com exceções do projeto e registrar com app.UseMiddleware<GlobalExceptionMiddleware>().
- Garantir que JSON use
PropertyNamingPolicy.CamelCase para que o contrato saia como success, data, error, timestamp, code, message.