Skip to content

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()

    @paginate(ArticlePagination)
    async def list(self, request) -> list[ArticleSerializer]:
        return await self.get_queryset()

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()

Performance Considerations

Avoiding N+1 Queries

Serialization happens after fetching items. If your serializer accesses related fields, use select_related() or prefetch_related() to avoid N+1 queries:

# Bad - N+1 queries when serializer accesses article.author
@api.get("/articles")
@paginate(ArticlePagination)
async def list_articles(request) -> list[ArticleSerializer]:
    return Article.objects.all()

# Good - single query with JOIN
@api.get("/articles")
@paginate(ArticlePagination)
async def list_articles(request) -> list[ArticleSerializer]:
    return Article.objects.select_related("author").all()

# Good - prefetch for many-to-many
@api.get("/articles")
@paginate(ArticlePagination)
async def list_articles(request) -> list[ArticleSerializer]:
    return Article.objects.prefetch_related("tags").all()

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.