Pagination¶
Django-Bolt provides three pagination styles for handling large datasets efficiently.
PageNumber Pagination¶
Classic page-based pagination:
from django_bolt import BoltAPI, PageNumberPagination, paginate
api = BoltAPI()
class ArticlePagination(PageNumberPagination):
page_size = 20
max_page_size = 100
page_size_query_param = "page_size" # Allow client to customize
@api.get("/articles")
@paginate(ArticlePagination)
async def list_articles(request) -> list[ArticleSerializer]:
return Article.objects.all()
Response:
{
"count": 150,
"page": 1,
"page_size": 20,
"total_pages": 8,
"has_next": true,
"has_previous": false,
"next_page": 2,
"previous_page": null,
"items": [...]
}
Query: /articles?page=2&page_size=10
LimitOffset Pagination¶
Flexible offset-based pagination:
from django_bolt import LimitOffsetPagination, paginate
@api.get("/articles", response_model=list[ArticleSerializer])
@paginate(LimitOffsetPagination)
async def list_articles(request):
return Article.objects.all()
Query: /articles?limit=10&offset=20
Response includes limit, offset, total, has_next, has_previous.
Cursor Pagination¶
Efficient pagination for large datasets and real-time feeds:
from django_bolt import CursorPagination, paginate
class ArticlePagination(CursorPagination):
page_size = 20
ordering = "-created_at" # Required: field to paginate by
@api.get("/articles")
@paginate(ArticlePagination)
async def list_articles(request) -> list[ArticleSerializer]:
return Article.objects.all()
Query: /articles?cursor=eyJ2IjoxMDB9
Specifying the Serializer¶
Two equivalent ways to specify which serializer to use:
# Option 1: Return type annotation (recommended)
@api.get("/articles")
@paginate(ArticlePagination)
async def list_articles(request) -> list[ArticleSerializer]:
return Article.objects.all()
# Option 2: response_model parameter
@api.get("/articles", response_model=list[ArticleSerializer])
@paginate(ArticlePagination)
async def list_articles(request):
return Article.objects.all()
Both approaches work identically. The serializer automatically filters fields - only declared fields are included in the response.
ViewSet with Pagination¶
from django_bolt.views import ViewSet
@api.viewset("/articles")
class ArticleViewSet(ViewSet):
queryset = Article.objects.all()
pagination_class = ArticlePagination
async def list(self, request) -> list[ArticleSerializer]:
return await self.get_queryset()
pagination_class is applied automatically to list() on ViewSet,
ReadOnlyModelViewSet, and ModelViewSet. Keep using @paginate(...) for
function-based views or when you want a method-specific override.
ModelViewSet with Pagination¶
from django_bolt.views import ModelViewSet
@api.viewset("/articles")
class ArticleViewSet(ModelViewSet):
queryset = Article.objects.all()
serializer_class = ArticleDetailSerializer # For detail views
list_serializer_class = ArticleListSerializer # For list view
pagination_class = ArticlePagination # Automatically applied to list()
Custom response envelope¶
The default envelope is PaginatedResponse[T]. It has items, total,
has_next, has_previous and the fields of the pagination scheme. You can use
a different shape, for example the DRF shape count/next/previous/results. Set
response_class to a generic Serializer or msgspec.Struct. Then override
build_response(). The response body and the OpenAPI schema
(DRFPage_ArticleSerializer_) use the new shape:
from typing import Generic, TypeVar
T = TypeVar("T")
class DRFPage(Serializer, Generic[T]):
count: int
next: str | None
previous: str | None
results: list[T]
class DRFPagination(PageNumberPagination):
page_size = 20
response_class = DRFPage
def build_response(self, items, *, total, request, page, page_size, has_next, has_previous, **info):
base = f"{request.path}?page_size={page_size}&page="
return DRFPage(
count=total,
next=f"{base}{page + 1}" if has_next else None,
previous=f"{base}{page - 1}" if has_previous else None,
results=items,
)
build_response() gets the serialized items, the total, the request and
the has_next and has_previous flags. It also gets the fields of the scheme
as keyword arguments:
- Page number:
page,page_size,total_pages,next_page,previous_page - Limit/offset:
limit,offset - Cursor:
page_size,next_cursor
Performance Considerations¶
Avoiding N+1 Queries¶
If the item type is a Bolt Serializer, Bolt applies the loading plan of the
serializer to the page QuerySet. The plan uses select_related for ForeignKey
and OneToOne fields, prefetch_related for many-valued relations, and
Config.annotations. Nested relations cost a small, fixed number of queries
for each page. See
Loading a QuerySet with from_models().
Bolt keeps the loads that you apply. Bolt does not apply them a second time. This code is correct:
@api.get("/articles")
@paginate(ArticlePagination)
async def list_articles(request) -> list[ArticleSerializer]:
return Article.objects.select_related("author").prefetch_related("tags")
Cursor vs PageNumber for Large Datasets¶
For tables with millions of rows, prefer CursorPagination over PageNumberPagination. Cursor pagination uses indexed columns for efficient seeking, while page number pagination requires counting total rows.