initial commit
This commit is contained in:
316
docs/user-guide/api/pagination.md
Normal file
316
docs/user-guide/api/pagination.md
Normal file
@ -0,0 +1,316 @@
|
||||
# API Pagination
|
||||
|
||||
This guide shows you how to add pagination to your API endpoints using the boilerplate's built-in utilities. Pagination helps you handle large datasets efficiently.
|
||||
|
||||
## Quick Start
|
||||
|
||||
Here's how to add basic pagination to any endpoint:
|
||||
|
||||
```python
|
||||
from fastcrud.paginated import PaginatedListResponse
|
||||
|
||||
@router.get("/", response_model=PaginatedListResponse[UserRead])
|
||||
async def get_users(
|
||||
page: int = 1,
|
||||
items_per_page: int = 10,
|
||||
db: Annotated[AsyncSession, Depends(async_get_db)]
|
||||
):
|
||||
users = await crud_users.get_multi(
|
||||
db=db,
|
||||
offset=(page - 1) * items_per_page,
|
||||
limit=items_per_page,
|
||||
schema_to_select=UserRead,
|
||||
return_as_model=True,
|
||||
return_total_count=True
|
||||
)
|
||||
|
||||
return paginated_response(
|
||||
crud_data=users,
|
||||
page=page,
|
||||
items_per_page=items_per_page
|
||||
)
|
||||
```
|
||||
|
||||
That's it! Your endpoint now returns paginated results with metadata.
|
||||
|
||||
## What You Get
|
||||
|
||||
The response includes everything frontends need:
|
||||
|
||||
```json
|
||||
{
|
||||
"data": [
|
||||
{
|
||||
"id": 1,
|
||||
"name": "John Doe",
|
||||
"username": "johndoe",
|
||||
"email": "john@example.com"
|
||||
}
|
||||
// ... more users
|
||||
],
|
||||
"total_count": 150,
|
||||
"has_more": true,
|
||||
"page": 1,
|
||||
"items_per_page": 10,
|
||||
"total_pages": 15
|
||||
}
|
||||
```
|
||||
|
||||
## Adding Filters
|
||||
|
||||
You can easily add filtering to paginated endpoints:
|
||||
|
||||
```python
|
||||
@router.get("/", response_model=PaginatedListResponse[UserRead])
|
||||
async def get_users(
|
||||
page: int = 1,
|
||||
items_per_page: int = 10,
|
||||
# Add filter parameters
|
||||
search: str | None = None,
|
||||
is_active: bool | None = None,
|
||||
tier_id: int | None = None,
|
||||
db: Annotated[AsyncSession, Depends(async_get_db)]
|
||||
):
|
||||
# Build filters
|
||||
filters = {}
|
||||
if search:
|
||||
filters["name__icontains"] = search # Search by name
|
||||
if is_active is not None:
|
||||
filters["is_active"] = is_active
|
||||
if tier_id:
|
||||
filters["tier_id"] = tier_id
|
||||
|
||||
users = await crud_users.get_multi(
|
||||
db=db,
|
||||
offset=(page - 1) * items_per_page,
|
||||
limit=items_per_page,
|
||||
schema_to_select=UserRead,
|
||||
return_as_model=True,
|
||||
return_total_count=True,
|
||||
**filters
|
||||
)
|
||||
|
||||
return paginated_response(
|
||||
crud_data=users,
|
||||
page=page,
|
||||
items_per_page=items_per_page
|
||||
)
|
||||
```
|
||||
|
||||
Now you can call:
|
||||
|
||||
- `/users/?search=john` - Find users with "john" in their name
|
||||
- `/users/?is_active=true` - Only active users
|
||||
- `/users/?tier_id=1&page=2` - Users in tier 1, page 2
|
||||
|
||||
## Adding Sorting
|
||||
|
||||
Add sorting options to your paginated endpoints:
|
||||
|
||||
```python
|
||||
@router.get("/", response_model=PaginatedListResponse[UserRead])
|
||||
async def get_users(
|
||||
page: int = 1,
|
||||
items_per_page: int = 10,
|
||||
# Add sorting parameters
|
||||
sort_by: str = "created_at",
|
||||
sort_order: str = "desc",
|
||||
db: Annotated[AsyncSession, Depends(async_get_db)]
|
||||
):
|
||||
users = await crud_users.get_multi(
|
||||
db=db,
|
||||
offset=(page - 1) * items_per_page,
|
||||
limit=items_per_page,
|
||||
schema_to_select=UserRead,
|
||||
return_as_model=True,
|
||||
return_total_count=True,
|
||||
sort_columns=sort_by,
|
||||
sort_orders=sort_order
|
||||
)
|
||||
|
||||
return paginated_response(
|
||||
crud_data=users,
|
||||
page=page,
|
||||
items_per_page=items_per_page
|
||||
)
|
||||
```
|
||||
|
||||
Usage:
|
||||
|
||||
- `/users/?sort_by=name&sort_order=asc` - Sort by name A-Z
|
||||
- `/users/?sort_by=created_at&sort_order=desc` - Newest first
|
||||
|
||||
## Validation
|
||||
|
||||
Add validation to prevent issues:
|
||||
|
||||
```python
|
||||
from fastapi import Query
|
||||
|
||||
@router.get("/", response_model=PaginatedListResponse[UserRead])
|
||||
async def get_users(
|
||||
page: Annotated[int, Query(ge=1)] = 1, # Must be >= 1
|
||||
items_per_page: Annotated[int, Query(ge=1, le=100)] = 10, # Between 1-100
|
||||
db: Annotated[AsyncSession, Depends(async_get_db)]
|
||||
):
|
||||
# Your pagination logic here
|
||||
```
|
||||
|
||||
## Complete Example
|
||||
|
||||
Here's a full-featured paginated endpoint:
|
||||
|
||||
```python
|
||||
@router.get("/", response_model=PaginatedListResponse[UserRead])
|
||||
async def get_users(
|
||||
# Pagination
|
||||
page: Annotated[int, Query(ge=1)] = 1,
|
||||
items_per_page: Annotated[int, Query(ge=1, le=100)] = 10,
|
||||
|
||||
# Filtering
|
||||
search: Annotated[str | None, Query(max_length=100)] = None,
|
||||
is_active: bool | None = None,
|
||||
tier_id: int | None = None,
|
||||
|
||||
# Sorting
|
||||
sort_by: str = "created_at",
|
||||
sort_order: str = "desc",
|
||||
|
||||
db: Annotated[AsyncSession, Depends(async_get_db)]
|
||||
):
|
||||
"""Get paginated users with filtering and sorting."""
|
||||
|
||||
# Build filters
|
||||
filters = {"is_deleted": False} # Always exclude deleted users
|
||||
|
||||
if is_active is not None:
|
||||
filters["is_active"] = is_active
|
||||
if tier_id:
|
||||
filters["tier_id"] = tier_id
|
||||
|
||||
# Handle search
|
||||
search_criteria = []
|
||||
if search:
|
||||
from sqlalchemy import or_, func
|
||||
search_criteria = [
|
||||
or_(
|
||||
func.lower(User.name).contains(search.lower()),
|
||||
func.lower(User.username).contains(search.lower()),
|
||||
func.lower(User.email).contains(search.lower())
|
||||
)
|
||||
]
|
||||
|
||||
users = await crud_users.get_multi(
|
||||
db=db,
|
||||
offset=(page - 1) * items_per_page,
|
||||
limit=items_per_page,
|
||||
schema_to_select=UserRead,
|
||||
return_as_model=True,
|
||||
return_total_count=True,
|
||||
sort_columns=sort_by,
|
||||
sort_orders=sort_order,
|
||||
**filters,
|
||||
**{"filter_criteria": search_criteria} if search_criteria else {}
|
||||
)
|
||||
|
||||
return paginated_response(
|
||||
crud_data=users,
|
||||
page=page,
|
||||
items_per_page=items_per_page
|
||||
)
|
||||
```
|
||||
|
||||
This endpoint supports:
|
||||
|
||||
- `/users/` - First 10 users
|
||||
- `/users/?page=2&items_per_page=20` - Page 2, 20 items
|
||||
- `/users/?search=john&is_active=true` - Active users named john
|
||||
- `/users/?sort_by=name&sort_order=asc` - Sorted by name
|
||||
|
||||
## Simple List (No Pagination)
|
||||
|
||||
Sometimes you just want a simple list without pagination:
|
||||
|
||||
```python
|
||||
@router.get("/all", response_model=list[UserRead])
|
||||
async def get_all_users(
|
||||
limit: int = 100, # Prevent too many results
|
||||
db: Annotated[AsyncSession, Depends(async_get_db)]
|
||||
):
|
||||
users = await crud_users.get_multi(
|
||||
db=db,
|
||||
limit=limit,
|
||||
schema_to_select=UserRead,
|
||||
return_as_model=True
|
||||
)
|
||||
return users["data"]
|
||||
```
|
||||
|
||||
## Performance Tips
|
||||
|
||||
1. **Always set a maximum page size**:
|
||||
```python
|
||||
items_per_page: Annotated[int, Query(ge=1, le=100)] = 10 # Max 100 items
|
||||
```
|
||||
|
||||
2. **Use `schema_to_select` to only fetch needed fields**:
|
||||
```python
|
||||
users = await crud_users.get_multi(
|
||||
schema_to_select=UserRead, # Only fetch UserRead fields
|
||||
return_as_model=True
|
||||
)
|
||||
```
|
||||
|
||||
3. **Add database indexes** for columns you sort by:
|
||||
```sql
|
||||
-- In your migration
|
||||
CREATE INDEX idx_users_created_at ON users(created_at);
|
||||
CREATE INDEX idx_users_name ON users(name);
|
||||
```
|
||||
|
||||
## Common Patterns
|
||||
|
||||
### Admin List with All Users
|
||||
```python
|
||||
@router.get("/admin", dependencies=[Depends(get_current_superuser)])
|
||||
async def get_all_users_admin(
|
||||
include_deleted: bool = False,
|
||||
page: int = 1,
|
||||
items_per_page: int = 50,
|
||||
db: Annotated[AsyncSession, Depends(async_get_db)]
|
||||
):
|
||||
filters = {}
|
||||
if not include_deleted:
|
||||
filters["is_deleted"] = False
|
||||
|
||||
users = await crud_users.get_multi(db=db, **filters)
|
||||
return paginated_response(users, page, items_per_page)
|
||||
```
|
||||
|
||||
### User's Own Items
|
||||
```python
|
||||
@router.get("/my-posts", response_model=PaginatedListResponse[PostRead])
|
||||
async def get_my_posts(
|
||||
page: int = 1,
|
||||
items_per_page: int = 10,
|
||||
current_user: Annotated[dict, Depends(get_current_user)],
|
||||
db: Annotated[AsyncSession, Depends(async_get_db)]
|
||||
):
|
||||
posts = await crud_posts.get_multi(
|
||||
db=db,
|
||||
author_id=current_user["id"], # Only user's own posts
|
||||
offset=(page - 1) * items_per_page,
|
||||
limit=items_per_page
|
||||
)
|
||||
return paginated_response(posts, page, items_per_page)
|
||||
```
|
||||
|
||||
## What's Next
|
||||
|
||||
Now that you understand pagination:
|
||||
|
||||
- **[Database CRUD](../database/crud.md)** - Learn more about the CRUD operations
|
||||
- **[Database Schemas](../database/schemas.md)** - Create schemas for your data
|
||||
- **[Authentication](../authentication/index.md)** - Add user authentication to your endpoints
|
||||
|
||||
The boilerplate makes pagination simple - just use these patterns!
|
||||
Reference in New Issue
Block a user