Response and Exception Handling Guide
Overview
Taruvi uses a standardized approach for handling responses and exceptions across all API endpoints.
Error Handling
Use raise with exception classes:
from ninja import Router
from base.responses.exceptions import (
ValidationException,
ResourceNotFoundException,
PermissionException,
ConflictException,
AppException
)
router = Router()
@router.get("/items/{id}/")
def get_item(request, id: int):
# Validation error (400)
if id < 0:
raise ValidationException(
message="Invalid ID",
detail="ID must be a positive integer"
)
# Not found error (404)
try:
item = Item.objects.get(id=id)
except Item.DoesNotExist:
raise ResourceNotFoundException(
message="Item not found",
detail=f"No item exists with ID {id}"
)
# Permission error (403)
if not request.user.has_perm('view_item'):
raise PermissionException(
message="Permission denied",
detail="You don't have permission to view this item"
)
# Success response
return 200, SuccessDataResponse(
message="Item retrieved successfully",
data=item
)
Success Responses
Use tuple returns with response schemas:
from base.responses import SuccessDataResponse, SuccessResponse
# Data response
return 200, SuccessDataResponse(
message="Items retrieved successfully",
data=items,
total=count
)
# Simple message response
return 200, SuccessResponse(
message="Operation completed successfully"
)
# Created response
return 201, SuccessDataResponse(
message="Item created successfully",
data=new_item
)
# No content (DELETE)
return 204, None
Exception Classes
All exceptions inherit from AppException and automatically map to HTTP status codes.
| Exception Class | HTTP Status | Error Code | Use Case |
|---|---|---|---|
ValidationException | 400 | VALIDATION_ERROR | Invalid input data |
AuthenticationException | 401 | AUTHENTICATION_FAILED | Authentication required/failed |
PermissionException | 403 | PERMISSION_DENIED | Insufficient permissions |
ResourceNotFoundException | 404 | NOT_FOUND | Resource doesn't exist |
ConflictException | 409 | CONFLICT | Resource conflict |
DuplicateEntryException | 409 | DUPLICATE_ENTRY | Duplicate resource |
BusinessLogicException | 422 | BUSINESS_LOGIC_ERROR | Business rule violation |
RateLimitException | 429 | RATE_LIMIT_EXCEEDED | Too many requests |
ServiceUnavailableException | 503 | SERVICE_UNAVAILABLE | Service unavailable |
GatewayTimeoutException | 504 | GATEWAY_TIMEOUT | Request timeout |
AppException | 500 | INTERNAL_ERROR | Generic server error |
Exception Parameters
All exceptions accept these parameters:
raise ValidationException(
message="Human-readable error message", # Required
detail="Additional context or technical details", # Optional
errors={"field": ["Error 1", "Error 2"]}, # Optional - field-level errors
data={"key": "value"} # Optional - additional error context
)
Response Formats
Success Response Format
{
"status": "success",
"message": "Operation completed successfully",
"data": {...},
"total": 100
}
Error Response Format
Exceptions are automatically converted to this format:
{
"status": "error",
"code": "VALIDATION_ERROR",
"message": "Validation failed",
"detail": "Additional error context",
"errors": {
"field_name": ["Error message"]
}
}
Best Practices
✅ DO
# Use specific exception classes
raise ValidationException(message="Invalid email format")
# Provide helpful error messages
raise ResourceNotFoundException(
message="User not found",
detail=f"No user exists with ID {user_id}"
)
# Use tuple returns for success
return 200, SuccessDataResponse(message="Success", data=result)
❌ DON'T
# Don't return raw dicts
return {"error": "Something went wrong"} # ❌
# Don't use generic exceptions without context
raise AppException() # ❌ - No message
# Don't return error tuples
return 400, {"error": "Invalid"} # ❌ - Use raise instead
Common Patterns
Validation Errors
# Single validation error
raise ValidationException(
message="Invalid input",
detail="Email format is invalid"
)
# Multiple field errors
raise ValidationException(
message="Validation failed",
errors={
"email": ["Invalid format"],
"age": ["Must be at least 18"]
}
)
Not Found Errors
try:
obj = Model.objects.get(id=obj_id)
except Model.DoesNotExist:
raise ResourceNotFoundException(
message=f"{Model.__name__} not found",
detail=f"No {Model.__name__} exists with ID {obj_id}"
)
Permission Errors
if not request.user.has_perm('app.permission'):
raise PermissionException(
message="Permission denied",
detail="You need 'permission' to perform this action"
)
Conflict Errors
if Model.objects.filter(email=email).exists():
raise DuplicateEntryException(
message="Email already exists",
detail=f"A user with email '{email}' already exists"
)
Testing
Testing Exception Handling
def test_validation_error():
with pytest.raises(ValidationException) as exc_info:
validate_data(invalid_data)
assert exc_info.value.message == "Validation failed"
assert exc_info.value.code == ErrorCode.VALIDATION_ERROR
Testing API Responses
def test_api_error_response(client):
response = client.get('/api/items/999/')
assert response.status_code == 404
assert response.json()['status'] == 'error'
assert response.json()['code'] == 'NOT_FOUND'
assert 'message' in response.json()
Summary
- Use
raisefor errors with specific exception classes - Use tuple returns for success responses
- Always provide meaningful error messages and details
- Let the exception handler convert exceptions to proper responses
For more examples, see:
cloud_site/data/api/routers/data.py- API examplesbase/responses/exceptions.py- Exception classes