Platform 07 · Study guide
Return one consistent JSON envelope from every controller so frontend and mobile clients stay simple.
Without a shared helper, every controller invents its own JSON shape ({"ok": true}, {"data": ...}, bare lists, …). Clients then need special cases.
tpy.http.ApiResponse always returns a FastAPI JSONResponse with a fixed envelope:
| Field | Success | Error |
|---|---|---|
success | true | false |
message | optional text | error summary |
data | payload | usually null |
meta | pagination / extras | — |
errors | — | field map or list |
If tamilPY is installed, import it in any controller:
from tpy.http import ApiResponse
Generated projects already depend on FastAPI, which ApiResponse uses for JSONResponse.
tpy.runtime.response.Response still exists. Prefer ApiResponse for new FastAPI controllers.ApiResponse.payload(...) to build a plain dict.JSONResponse with an HTTP status code.returns that response — FastAPI sends JSON to the client.from tpy.http import ApiResponse
# 200 OK
return ApiResponse.success(
{"id": "u1", "email": "asha@example.com"},
message="User loaded",
)
# 201 Created
return ApiResponse.created(
{"id": "u1"},
message="User created",
)
Example JSON body for success:
{
"success": true,
"message": "User loaded",
"data": {"id": "u1", "email": "asha@example.com"},
"meta": null
}
from fastapi import APIRouter
from tpy.http import ApiResponse
router = APIRouter(prefix="/users", tags=["users"])
@router.get("/{user_id}")
def show_user(user_id: str):
user = service.find(user_id)
if not user:
return ApiResponse.not_found("User not found")
return ApiResponse.success(user, message="OK")
@router.post("/")
def create_user(payload: UserCreate):
row = service.create(payload.model_dump())
return ApiResponse.created(row)
paginated() accepts a Query Builder paginator (to_dict()) or a dict with data + meta:
page = repo.query().paginate(page=1, per_page=20) return ApiResponse.paginated(page, message="Users page")
{
"success": true,
"message": "Users page",
"data": [ /* rows */ ],
"meta": {
"current_page": 1,
"per_page": 20,
"total": 105
/* …paginator fields */
}
}
# Generic error (default 400)
return ApiResponse.error("Email already used", status_code=409)
# Shortcuts
return ApiResponse.not_found() # 404
return ApiResponse.unauthorized() # 401
return ApiResponse.forbidden() # 403
return ApiResponse.no_content() # 204 empty body
Error JSON shape:
{
"success": false,
"message": "Email already used",
"data": null,
"errors": {}
}
Pair with the Validation engine:
from tpy.validation import validate, ValidationException
from tpy.http import ApiResponse
try:
data = validate(payload, {
"email": "required|email",
"age": "required|integer|min:18",
})
except ValidationException as exc:
return ApiResponse.validation_error(exc)
return ApiResponse.success(data)
validation_error reads exc.first_messages() when you pass the exception, and returns HTTP 422 with an errors object keyed by field.
| Method | Status | When |
|---|---|---|
success | 200 | Normal read/update |
created | 201 | Resource created |
paginated | 200 | List with meta |
error | 400+ | Custom failure |
validation_error | 422 | Invalid input |
not_found | 404 | Missing resource |
unauthorized | 401 | Auth missing/bad |
forbidden | 403 | Authenticated but denied |
no_content | 204 | Delete with empty body |
payload / json | custom | Build envelope manually |
dict from some routes and ApiResponse from others — clients break.message and leaving errors empty when you have field-level problems.paginated() needs a paginator/data+meta dict — not a bare list.