Docs / Guides / ApiResponse helpers

Platform 07 · Study guide

ApiResponse helpers

Return one consistent JSON envelope from every controller so frontend and mobile clients stay simple.

What you will learn
  • What the success/error JSON envelope looks like
  • How to return ApiResponse from a FastAPI controller
  • How pagination and validation errors map into that envelope

1. Why ApiResponse exists

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:

FieldSuccessError
successtruefalse
messageoptional texterror summary
datapayloadusually null
metapagination / extras
errorsfield map or list

2. Setup (no extra install)

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.

Soft-break: older tpy.runtime.response.Response still exists. Prefer ApiResponse for new FastAPI controllers.

3. How it works (internally)

  1. Helpers call ApiResponse.payload(...) to build a plain dict.
  2. That dict is wrapped in FastAPI JSONResponse with an HTTP status code.
  3. Your route returns that response — FastAPI sends JSON to the client.

4. Success responses

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
}

5. Use inside a controller

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)

6. Pagination

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 */
  }
}

7. Errors

# 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": {}
}

8. Validation errors (422)

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.

9. Method cheat sheet

MethodStatusWhen
success200Normal read/update
created201Resource created
paginated200List with meta
error400+Custom failure
validation_error422Invalid input
not_found404Missing resource
unauthorized401Auth missing/bad
forbidden403Authenticated but denied
no_content204Delete with empty body
payload / jsoncustomBuild envelope manually

10. Common mistakes

  • Returning a raw dict from some routes and ApiResponse from others — clients break.
  • Putting error details only in message and leaving errors empty when you have field-level problems.
  • Forgetting that paginated() needs a paginator/data+meta dict — not a bare list.