Pagination contract is not standardized across endpoints
This commit is contained in:
@@ -461,6 +461,64 @@ class BansByCountryResponse(BaseModel):
|
||||
|
||||
5. **No ad-hoc wrapper objects** — Don't invent new response shapes. Use the patterns above.
|
||||
|
||||
### Standardized Pagination Query Parameters
|
||||
|
||||
All paginated endpoints follow a consistent query parameter contract:
|
||||
|
||||
| Parameter | Type | Constraints | Default | Notes |
|
||||
|---|---|---|---|---|
|
||||
| `page` | int | ≥ 1 | `1` | 1-based page number (not 0-based offset). |
|
||||
| `page_size` | int | 1–500 | `100` | Items per page. Clients may request smaller pages for UI reasons. |
|
||||
|
||||
**Implementation:**
|
||||
|
||||
```python
|
||||
from fastapi import Query
|
||||
from app.utils.constants import DEFAULT_PAGE_SIZE
|
||||
|
||||
@router.get("/items")
|
||||
async def get_items(
|
||||
page: int = Query(default=1, ge=1, description="1-based page number."),
|
||||
page_size: int = Query(
|
||||
default=DEFAULT_PAGE_SIZE,
|
||||
ge=1,
|
||||
le=500,
|
||||
description="Items per page (max 500).",
|
||||
),
|
||||
):
|
||||
# Compute offset for database query
|
||||
offset = (page - 1) * page_size
|
||||
items = await db.fetch("SELECT * FROM items LIMIT ? OFFSET ?", page_size, offset)
|
||||
total = await db.fetchval("SELECT COUNT(*) FROM items")
|
||||
|
||||
return PaginatedListResponse(
|
||||
items=items,
|
||||
total=total,
|
||||
page=page,
|
||||
page_size=page_size,
|
||||
)
|
||||
```
|
||||
|
||||
**Helper functions** are available in `app.utils.pagination`:
|
||||
|
||||
```python
|
||||
from app.utils.pagination import get_offset, compute_total_pages
|
||||
|
||||
# Calculate database offset from page and page_size
|
||||
offset = get_offset(page, page_size) # Equivalent to (page - 1) * page_size
|
||||
|
||||
# Calculate total pages for rendering pagination UI (optional)
|
||||
total_pages = compute_total_pages(total, page_size)
|
||||
```
|
||||
|
||||
**Rules:**
|
||||
|
||||
1. **Use 1-based pages** — Not 0-based offsets. Page 1 is always the first page.
|
||||
2. **Always provide defaults** — Use `DEFAULT_PAGE_SIZE` (100) and initial page 1.
|
||||
3. **Cap maximum page_size at 500** — Prevents accidental DoS from enormous requests.
|
||||
4. **Respond with `PaginatedListResponse[T]`** — Must include `items`, `total`, `page`, `page_size`.
|
||||
5. **Never include `total_pages` in responses** — The frontend can calculate it as `Math.ceil(total / page_size)`.
|
||||
|
||||
---
|
||||
|
||||
## 5. Pydantic Models
|
||||
|
||||
Reference in New Issue
Block a user