Skip to Content

Pagination

DRF pagination affects how the OpenAPI schema is generated and what TypeScript types your clients receive. Understanding when pagination is applied helps avoid type mismatches.

How Pagination Works with Generation

Default behavior

DRF ViewSets inherit pagination_class from REST_FRAMEWORK settings or the ViewSet itself. When drf-spectacular detects many=True in responses, it wraps the schema in a paginated response:

# Paginated response schema (default for list actions) PaginatedUserList: type: object properties: count: type: integer page: type: integer pages: type: integer page_size: type: integer has_next: type: boolean has_previous: type: boolean next_page: type: integer nullable: true previous_page: type: integer nullable: true results: type: array items: $ref: '#/components/schemas/User'

Generated TypeScript:

interface PaginatedUserList { count: number; page: number; pages: number; page_size: number; has_next: boolean; has_previous: boolean; next_page: number | null; previous_page: number | null; results: User[]; }

When pagination applies

ActionPaginatedWhy
list (ViewSet)YesInherits ViewSet pagination
@action with many=TrueYesInherits ViewSet pagination
@action with pagination_class=NoneNoExplicitly disabled
retrieveNoSingle object
create / update / destroyNoSingle object

Patterns

Standard paginated list

For large collections where frontend needs page controls:

class UserViewSet(viewsets.ModelViewSet): queryset = User.objects.all() serializer_class = UserSerializer # pagination_class inherited from REST_FRAMEWORK settings # list() is automatically paginated

Frontend usage:

const { data } = useUsersList(api, { page: 1 }); // data.count → 150 // data.page → 1 // data.pages → 15 // data.page_size → 10 // data.has_next → true // data.has_previous → false // data.next_page → 2 // data.previous_page → null // data.results → User[]

Custom action with pagination

When a custom action should be paginated:

@extend_schema( responses={200: UserSerializer(many=True)}, tags=["users"], ) @action(detail=False, methods=['get']) def active(self, request): """List active users (paginated).""" queryset = self.filter_queryset( User.objects.filter(is_active=True) ) page = self.paginate_queryset(queryset) if page is not None: serializer = UserSerializer(page, many=True) return self.get_paginated_response(serializer.data) serializer = UserSerializer(queryset, many=True) return Response(serializer.data)

Custom action without pagination

For small fixed lists (see Array Responses):

@extend_schema( responses={200: StatusSerializer(many=True)}, tags=["users"], ) @action( detail=False, methods=['get'], pagination_class=None, # ← No pagination ) def statuses(self, request): """List available statuses (not paginated).""" statuses = [ {"key": "active", "label": "Active"}, {"key": "inactive", "label": "Inactive"}, ] serializer = StatusSerializer(statuses, many=True) return Response(serializer.data)

Custom Pagination Classes

Per-ViewSet pagination

from rest_framework.pagination import PageNumberPagination class LargeResultsPagination(PageNumberPagination): page_size = 100 page_size_query_param = 'page_size' max_page_size = 1000 class OrderViewSet(viewsets.ModelViewSet): queryset = Order.objects.all() serializer_class = OrderSerializer pagination_class = LargeResultsPagination

Per-action pagination

class SmallPagePagination(PageNumberPagination): page_size = 10 class DashboardViewSet(viewsets.GenericViewSet): serializer_class = DashboardSerializer @extend_schema( responses={200: NotificationSerializer(many=True)}, tags=["dashboard"], ) @action( detail=False, methods=['get'], pagination_class=SmallPagePagination, ) def recent_notifications(self, request): queryset = Notification.objects.filter(user=request.user)[:10] page = self.paginate_queryset(queryset) serializer = NotificationSerializer(page, many=True) return self.get_paginated_response(serializer.data)

Built-in Pagination Classes

Django-CFG provides several pre-configured pagination classes that you can use directly:

DefaultPagination

The default pagination class used by Django-CFG. Configured via REST_FRAMEWORK settings in your Django-CFG configuration.

from django_cfg.middleware.pagination import DefaultPagination class UserViewSet(viewsets.ModelViewSet): queryset = User.objects.all() serializer_class = UserSerializer pagination_class = DefaultPagination # or omit — this is the default

LargePagination

Returns 500 items per page. Useful for admin dashboards or bulk data views:

from django_cfg.middleware.pagination import LargePagination class OrderViewSet(viewsets.ModelViewSet): queryset = Order.objects.all() serializer_class = OrderSerializer pagination_class = LargePagination

SmallPagination

Returns 20 items per page. Ideal for mobile-first APIs or sidebar widgets:

from django_cfg.middleware.pagination import SmallPagination class NotificationViewSet(viewsets.ModelViewSet): queryset = Notification.objects.all() serializer_class = NotificationSerializer pagination_class = SmallPagination

NoPagination

Disables pagination entirely. Use for endpoints that return small, fixed-size collections:

from django_cfg.middleware.pagination import NoPagination class StatusViewSet(viewsets.GenericViewSet): serializer_class = StatusSerializer pagination_class = NoPagination def list(self, request): statuses = [{"key": "active"}, {"key": "inactive"}] serializer = self.get_serializer(statuses, many=True) return Response(serializer.data)
ClassPage SizeUse Case
DefaultPagination100 (configurable)General-purpose lists
LargePagination500Admin dashboards, bulk exports
SmallPagination20Mobile APIs, widgets, sidebars
NoPaginationN/A (disabled)Small fixed collections

Common Issues

Action returns array but schema says paginated

Problem: Frontend gets "expected object, received array".

Cause: pagination_class inherited from ViewSet but action returns Response(serializer.data) without self.get_paginated_response().

Fix: Add pagination_class=None to the @action decorator:

@action(detail=False, methods=['get'], pagination_class=None) def my_list(self, request): ...

Schema says array but action paginates

Problem: Frontend gets an object with count/results but expects an array.

Cause: Action uses self.paginate_queryset() but has pagination_class=None.

Fix: Remove pagination_class=None from the @action decorator so the schema matches the actual response.

Inconsistent pagination across actions

Problem: Some actions in a ViewSet are paginated, others aren’t, causing confusion.

Fix: Be explicit about pagination on every @action:

class ItemViewSet(viewsets.GenericViewSet): serializer_class = ItemSerializer # Explicitly paginated @extend_schema(responses={200: ItemSerializer(many=True)}, tags=["items"]) @action(detail=False, methods=['get']) # Inherits ViewSet pagination def all_items(self, request): ... # Explicitly not paginated @extend_schema(responses={200: ItemSerializer(many=True)}, tags=["items"]) @action(detail=False, methods=['get'], pagination_class=None) def featured(self, request): ...

Next Steps

Last updated on