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
| Action | Paginated | Why |
|---|---|---|
list (ViewSet) | Yes | Inherits ViewSet pagination |
@action with many=True | Yes | Inherits ViewSet pagination |
@action with pagination_class=None | No | Explicitly disabled |
retrieve | No | Single object |
create / update / destroy | No | Single 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 paginatedFrontend 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 = LargeResultsPaginationPer-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 defaultLargePagination
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 = LargePaginationSmallPagination
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 = SmallPaginationNoPagination
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)| Class | Page Size | Use Case |
|---|---|---|
DefaultPagination | 100 (configurable) | General-purpose lists |
LargePagination | 500 | Admin dashboards, bulk exports |
SmallPagination | 20 | Mobile APIs, widgets, sidebars |
NoPagination | N/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
- DRF ViewSets & Serializers — General ViewSet patterns
- Array Responses — Returning arrays from
@actionmethods - Nested Serializers — Handling
allOfand nested types