Skip to main content
The Happy Habitat API uses standardized error codes to help clients handle errors programmatically. Each error code is associated with an HTTP status code.

Error Code Reference

400 Bad Request

Returned when the request contains invalid data or an operation cannot be performed.
BAD_REQUEST
400
General bad request error. The request was malformed or contains invalid parameters.Triggered by: ArgumentException in middleware or explicit controller returnsMessage: Varies based on the specific validation failure
VALIDATION_ERROR
400
One or more model validation errors occurred. Check the errors field for field-specific validation messages.Triggered by: ASP.NET Core model validation (DataAnnotations)Default message: “Uno o más campos tienen errores de validación.”Common causes:
  • Missing required fields
  • Invalid data format (e.g., email, date)
  • Data constraints violated (e.g., string too long, number out of range)
Implementation: Configured in Program.cs:28-43 via InvalidModelStateResponseFactory
INVALID_OPERATION
400
The requested operation is not allowed in the current state or context.Triggered by: InvalidOperationException thrown in business logicMessage: Exception message from the thrown InvalidOperationExceptionExample scenarios:
  • Attempting to delete a resource that has dependencies
  • Performing an action on a resource in the wrong state
  • Business rule violation (e.g., “Usuario no está registrado como residente.”)
Implementation: Exception handled in ExceptionHandlingMiddleware.cs:43

401 Unauthorized

Returned when authentication is required but not provided or is invalid.
UNAUTHORIZED
401
Authentication required or authentication credentials are invalid.Triggered by:
  • UnauthorizedAccessException in middleware
  • Explicit Unauthorized() returns in controllers
  • JWT authentication failure
Default message: “No autorizado.”Common causes:
  • Missing Authorization header
  • Expired JWT token
  • Invalid JWT token signature
  • Invalid username or password (login endpoint)
Client action: Redirect user to login or refresh authentication tokenImplementation: Exception handled in ExceptionHandlingMiddleware.cs:46

403 Forbidden

Returned when the authenticated user does not have permission to access the resource.
Current status: The API uses Forbid() in some controllers but does not return a structured ApiErrorResponse for 403 errors.Controllers return ASP.NET Core’s default 403 response without the standard error format.Common scenarios:
  • Resident attempting to access another resident’s data
  • User without admin role attempting admin operations
Future improvement: Consider implementing a standardized 403 error response format.

404 Not Found

Returned when the requested resource does not exist.
NOT_FOUND
404
The requested resource could not be found.Triggered by:
  • KeyNotFoundException in middleware
  • Explicit NotFound() or NotFoundApiError() in controllers
Default message: “Recurso no encontrado.”Common causes:
  • Invalid resource ID
  • Resource was deleted
  • Incorrect endpoint URL
Implementation:
  • Exception handled in ExceptionHandlingMiddleware.cs:45
  • Extension method in ControllerBaseExtensions.cs:27

409 Conflict

Returned when the request conflicts with the current state of the server.
Current status: The API has a ConflictApiError() extension method available but is not actively used in the current codebase.Typical use cases:
  • Duplicate resource creation (e.g., username already exists)
  • Optimistic concurrency conflicts
  • Resource state conflicts
Implementation: Extension method available in ControllerBaseExtensions.cs:40

500 Internal Server Error

Returned when an unexpected error occurs on the server.
INTERNAL_ERROR
500
An unexpected internal server error occurred.Triggered by: Any unhandled exception that doesn’t match other specific exception typesProduction message: “Ha ocurrido un error interno. Intente más tarde.”Development message: Full exception message for debugging
In production environments, error details are limited for security. The message field will contain a generic error message. Use the traceId to investigate errors in server logs.
Implementation: Default case in ExceptionHandlingMiddleware.cs:52-56
DATABASE_ERROR
500
A database operation failed unexpectedly.Triggered by: DbUpdateException from Entity Framework CoreProduction message: “Error al guardar los datos. Intente de nuevo.”Development message: Inner exception message or main exception messageCommon causes:
  • Database constraint violations
  • Connection failures
  • Transaction timeouts
Note: This error indicates a server-side issue. Contact support with the traceId if the error persists.Implementation: Exception handled in ExceptionHandlingMiddleware.cs:47-51

HTTP Status Code Summary

HTTP StatusError CodesUsage
400BAD_REQUEST, INVALID_OPERATION, VALIDATION_ERRORInvalid data or operation not permitted
401UNAUTHORIZEDNot authenticated (missing or invalid token)
403(no structured error)Authenticated but lacking permission
404NOT_FOUNDResource not found
409(extension available but unused)Conflict (e.g., duplicate resource)
500INTERNAL_ERROR, DATABASE_ERRORInternal server error

Source Code Reference

Exception Handling Middleware

File: HappyHabitat.API/Middleware/ExceptionHandlingMiddleware.cs Key logic: Lines 36-70
var (statusCode, code, message) = exception switch
{
    InvalidOperationException inv => (HttpStatusCode.BadRequest, "INVALID_OPERATION", inv.Message),
    ArgumentException arg => (HttpStatusCode.BadRequest, "BAD_REQUEST", arg.Message),
    KeyNotFoundException => (HttpStatusCode.NotFound, "NOT_FOUND", "Recurso no encontrado."),
    UnauthorizedAccessException => (HttpStatusCode.Unauthorized, "UNAUTHORIZED", "No autorizado."),
    DbUpdateException dbEx => (
        HttpStatusCode.InternalServerError,
        "DATABASE_ERROR",
        _env.IsDevelopment() ? dbEx.InnerException?.Message ?? dbEx.Message : "Error al guardar los datos. Intente de nuevo."
    ),
    _ => (
        HttpStatusCode.InternalServerError,
        "INTERNAL_ERROR",
        _env.IsDevelopment() ? exception.Message : "Ha ocurrido un error interno. Intente más tarde."
    )
};

Model Validation Configuration

File: HappyHabitat.API/Program.cs Key logic: Lines 25-44
builder.Services.AddControllers()
    .ConfigureApiBehaviorOptions(options =>
    {
        options.InvalidModelStateResponseFactory = context =>
        {
            var errors = context.ModelState
                .Where(e => e.Value?.Errors.Count > 0)
                .ToDictionary(
                    kvp => kvp.Key,
                    kvp => kvp.Value!.Errors.Select(e => e.ErrorMessage).ToArray());
            var response = new ApiErrorResponse
            {
                Code = "VALIDATION_ERROR",
                Message = "Uno o más campos tienen errores de validación.",
                Errors = errors,
                TraceId = context.HttpContext.TraceIdentifier
            };
            return new BadRequestObjectResult(response);
        };
    });

Controller Extension Methods

File: HappyHabitat.API/Extensions/ControllerBaseExtensions.cs Available methods:
// Returns 400 Bad Request with custom code and message
public static ActionResult BadRequestApiError(this ControllerBase controller, string code, string message)

// Returns 404 Not Found with standard format
public static ActionResult NotFoundApiError(this ControllerBase controller, string message = "Recurso no encontrado.")

// Returns 409 Conflict with custom code and message
public static ActionResult ConflictApiError(this ControllerBase controller, string code, string message)

Common Error Scenarios

Scenario 1: Validation Failure

Request: POST /api/pets
{
  "name": "",
  "species": "dinosaur"
}
Response: 400 Bad Request
{
  "code": "VALIDATION_ERROR",
  "message": "Uno o más campos tienen errores de validación.",
  "errors": {
    "name": ["Name is required"],
    "species": ["Species must be one of: dog, cat, bird, other"]
  },
  "traceId": "0HN7GFQJ5KQVM:00000001"
}

Scenario 2: Resource Not Found

Request: GET /api/pets/99999 Response: 404 Not Found
{
  "code": "NOT_FOUND",
  "message": "Recurso no encontrado.",
  "errors": null,
  "traceId": "0HN7GFQJ5KQVM:00000002"
}

Scenario 3: Invalid Credentials

Request: POST /api/auth/login
{
  "username": "[email protected]",
  "password": "wrongpassword"
}
Response: 401 Unauthorized
{
  "code": "UNAUTHORIZED",
  "message": "Invalid username or password",
  "errors": null,
  "traceId": "0HN7GFQJ5KQVM:00000003"
}
Note: This specific endpoint returns a plain string message instead of ApiErrorResponse format.

Scenario 4: Expired Token

Request: GET /api/residents/me (with expired JWT) Response: 401 Unauthorized
{
  "code": "UNAUTHORIZED",
  "message": "No autorizado.",
  "errors": null,
  "traceId": "0HN7GFQJ5KQVM:00000004"
}

Scenario 5: Invalid Operation

Request: POST /api/comentarios Response: 400 Bad Request
{
  "code": "INVALID_OPERATION",
  "message": "Usuario no está registrado como residente.",
  "errors": null,
  "traceId": "0HN7GFQJ5KQVM:00000005"
}

Scenario 6: Database Error

Request: POST /api/communities (with constraint violation) Response: 500 Internal Server Error Production:
{
  "code": "DATABASE_ERROR",
  "message": "Error al guardar los datos. Intente de nuevo.",
  "errors": null,
  "traceId": "0HN7GFQJ5KQVM:00000006"
}
Development:
{
  "code": "DATABASE_ERROR",
  "message": "Cannot insert duplicate key in object 'dbo.Communities'. The duplicate key value is (12345).",
  "errors": null,
  "traceId": "0HN7GFQJ5KQVM:00000006"
}

Error Handling Best Practices

Use Error Codes

Rely on the code field for programmatic decision-making, not the message field which may change or be in different languages.

Display Messages

Show the message field to users as it’s designed to be human-readable. Note that messages are in Spanish.

Handle Field Errors

When errors is present (validation failures), display field-specific messages in your forms for better UX.

Include Trace IDs

Always include the traceId when reporting issues to support for faster resolution. It correlates with server logs.

Client Error Handling Template

interface ApiErrorResponse {
  code: string;
  message: string;
  errors?: Record<string, string[]> | null;
  traceId?: string | null;
}

class ApiError extends Error {
  constructor(
    public code: string,
    message: string,
    public errors?: Record<string, string[]> | null,
    public traceId?: string | null
  ) {
    super(message);
    this.name = 'ApiError';
  }
}

async function apiRequest<T>(url: string, options?: RequestInit): Promise<T> {
  const response = await fetch(url, {
    ...options,
    headers: {
      'Content-Type': 'application/json',
      ...options?.headers,
    },
  });

  if (!response.ok) {
    const error: ApiErrorResponse = await response.json();
    throw new ApiError(
      error.code,
      error.message,
      error.errors,
      error.traceId
    );
  }

  return response.json();
}

// Usage example
try {
  const data = await apiRequest('/api/pets', {
    method: 'POST',
    body: JSON.stringify({ name: 'Fluffy', species: 'cat' }),
  });
  console.log('Success:', data);
} catch (error) {
  if (error instanceof ApiError) {
    switch (error.code) {
      case 'VALIDATION_ERROR':
        console.error('Validation failed:', error.errors);
        // Display field-specific errors in UI
        break;
      case 'UNAUTHORIZED':
        console.error('Not authorized, redirecting to login');
        window.location.href = '/login';
        break;
      case 'NOT_FOUND':
        console.error('Resource not found');
        break;
      default:
        console.error('API error:', error.message);
        console.error('Trace ID:', error.traceId);
    }
  } else {
    console.error('Network error:', error);
  }
}

Build docs developers (and LLMs) love