API Response Envelopes and Pagination
This project uses a consistent pattern for API responses, ensuring that collection data is always accompanied by metadata and that simple operations return a standardized message format. This is achieved through specific "Public" envelope classes and a generic message schema.
Collection Envelopes
When the API returns a list of resources, such as users or items, it does not return a raw JSON array. Instead, it wraps the list in an "envelope" object. This pattern provides a consistent structure for the frontend and allows for metadata, specifically the total count of records, to be included alongside the data.
The two primary collection envelopes are defined in backend/app/models.py:
class UsersPublic(SQLModel):
data: list[UserPublic]
count: int
class ItemsPublic(SQLModel):
data: list[ItemPublic]
count: int
Structure and Purpose
data: A list of the actual resource objects (e.g.,UserPublicorItemPublic). These objects contain the fields safe for public consumption, excluding sensitive data like hashed passwords.count: An integer representing the total number of records available in the database that match the query criteria. This is crucial for frontend pagination components to calculate the total number of pages.
Pagination Implementation
Pagination is handled at the route level using skip and limit query parameters. The API implementation ensures that the count returned in the envelope reflects the total matching records, while the data list contains only the requested subset.
For example, in backend/app/api/routes/items.py, the read_items endpoint implements this logic:
@router.get("/", response_model=ItemsPublic)
def read_items(
session: SessionDep, current_user: CurrentUser, skip: int = 0, limit: int = 100
) -> Any:
# ... logic to determine count and items based on user permissions ...
# 1. Get total count
count_statement = select(func.count()).select_from(Item)
count = session.exec(count_statement).one()
# 2. Get paginated data
statement = (
select(Item).order_by(col(Item.created_at).desc()).offset(skip).limit(limit)
)
items = session.exec(statement).all()
# 3. Return wrapped in ItemsPublic envelope
return ItemsPublic(data=items, count=count)
This approach separates the concerns of data retrieval and total record counting, providing the client with everything needed to render paginated lists.
Standardized Status Messages
For operations that do not return a specific resource—such as deleting a record or updating a password—the API uses the Message class. This provides a simple, predictable JSON response indicating the result of the operation.
The Message class is defined in backend/app/models.py:
class Message(SQLModel):
message: str
Usage Examples
The Message schema is frequently used in DELETE and certain PATCH endpoints across the codebase:
-
Deleting an Item (
backend/app/api/routes/items.py):@router.delete("/{id}")
def delete_item(session: SessionDep, current_user: CurrentUser, id: uuid.UUID) -> Message:
# ... deletion logic ...
return Message(message="Item deleted successfully") -
Updating a Password (
backend/app/api/routes/users.py):@router.patch("/me/password", response_model=Message)
def update_password_me(
*, session: SessionDep, body: UpdatePassword, current_user: CurrentUser
) -> Any:
# ... password update logic ...
return Message(message="Password updated successfully")
By using the Message class, the API avoids returning empty responses or inconsistent strings, making the client-side response handling more robust.
Integration in Tests
The consistency of these envelopes is verified in the test suite. For instance, backend/tests/api/routes/test_items.py validates that the response from the items endpoint contains both the data array and the count field:
def test_read_items(
client: TestClient, superuser_token_headers: dict[str, str], db: Session
) -> None:
# ... setup ...
response = client.get(
f"{settings.API_V1_STR}/items/",
headers=superuser_token_headers,
)
assert response.status_code == 200
content = response.json()
assert "data" in content
assert "count" in content
assert len(content["data"]) >= 2
This ensures that any changes to the models or routes must maintain the expected envelope structure.