0
# Views & ViewSets
1
2
Django REST Framework views and viewsets provide the core infrastructure for handling HTTP requests and returning responses. The type stubs enable full type safety for view classes, mixins, and viewset operations with comprehensive generic type support.
3
4
## Core Imports
5
6
```python { .api }
7
from typing import Any, NoReturn, Sequence, Callable
8
from django.http import HttpRequest, HttpResponseBase
9
from django.db.models import QuerySet, Manager
10
from rest_framework.views import APIView
11
from rest_framework.generics import GenericAPIView, UsesQuerySet
12
from rest_framework.mixins import (
13
CreateModelMixin, ListModelMixin, RetrieveModelMixin,
14
UpdateModelMixin, DestroyModelMixin
15
)
16
from rest_framework.viewsets import ViewSet, GenericViewSet, ModelViewSet
17
from rest_framework.request import Request
18
from rest_framework.response import Response
19
from rest_framework.authentication import BaseAuthentication
20
from rest_framework.permissions import _PermissionClass, _SupportsHasPermission
21
from rest_framework.renderers import BaseRenderer
22
from rest_framework.parsers import BaseParser
23
from rest_framework.throttling import BaseThrottle
24
from rest_framework.negotiation import BaseContentNegotiation
25
from rest_framework.metadata import BaseMetadata
26
from rest_framework.versioning import BaseVersioning
27
from rest_framework.serializers import BaseSerializer
28
from rest_framework.filters import BaseFilterBackend
29
from rest_framework.pagination import BasePagination
30
from rest_framework.decorators import ViewSetAction
31
32
# Type variables
33
_MT_co = TypeVar("_MT_co", bound=Model, covariant=True)
34
_ViewFunc = Callable[..., HttpResponseBase]
35
```
36
37
## Base View Classes
38
39
### APIView
40
41
```python { .api }
42
class APIView(View):
43
"""Base class for all DRF views with comprehensive type safety."""
44
45
# Class-level configuration
46
authentication_classes: Sequence[type[BaseAuthentication]]
47
permission_classes: Sequence[_PermissionClass]
48
renderer_classes: Sequence[type[BaseRenderer]]
49
parser_classes: Sequence[type[BaseParser]]
50
throttle_classes: Sequence[type[BaseThrottle]]
51
content_negotiation_class: type[BaseContentNegotiation]
52
metadata_class: str | BaseMetadata | None
53
versioning_class: type[BaseVersioning] | None
54
55
# Request processing
56
def dispatch(self, request: Request, *args: Any, **kwargs: Any) -> Response: ...
57
def handle_exception(self, exc: Exception) -> Response: ...
58
def permission_denied(self, request: Request, message: str | None = None, code: str | None = None) -> NoReturn: ...
59
def throttled(self, request: Request, wait: float) -> NoReturn: ...
60
61
# Configuration methods
62
def get_authenticators(self) -> list[BaseAuthentication]: ...
63
def get_permissions(self) -> Sequence[_SupportsHasPermission]: ...
64
def get_renderers(self) -> list[BaseRenderer]: ...
65
def get_parsers(self) -> list[BaseParser]: ...
66
def get_throttles(self) -> list[BaseThrottle]: ...
67
def get_content_negotiator(self) -> BaseContentNegotiation: ...
68
def get_exception_handler(self) -> Callable: ...
69
70
# HTTP method handlers
71
def get(self, request: Request, *args: Any, **kwargs: Any) -> Response: ...
72
def post(self, request: Request, *args: Any, **kwargs: Any) -> Response: ...
73
def put(self, request: Request, *args: Any, **kwargs: Any) -> Response: ...
74
def patch(self, request: Request, *args: Any, **kwargs: Any) -> Response: ...
75
def delete(self, request: Request, *args: Any, **kwargs: Any) -> Response: ...
76
def head(self, request: Request, *args: Any, **kwargs: Any) -> Response: ...
77
def options(self, request: Request, *args: Any, **kwargs: Any) -> Response: ...
78
def trace(self, request: Request, *args: Any, **kwargs: Any) -> Response: ...
79
```
80
81
### GenericAPIView
82
83
```python { .api }
84
class GenericAPIView(APIView, UsesQuerySet[_MT_co]):
85
"""Generic view with model type support and common functionality."""
86
87
# Model and serializer configuration
88
queryset: QuerySet[_MT_co] | Manager[_MT_co] | None
89
serializer_class: type[BaseSerializer[_MT_co]] | None
90
91
# Lookup configuration
92
lookup_field: str # Default: 'pk'
93
lookup_url_kwarg: str | None
94
95
# Filtering and pagination
96
filter_backends: Sequence[type[BaseFilterBackend | BaseFilterProtocol[_MT_co]]]
97
pagination_class: type[BasePagination] | None
98
99
# Object retrieval
100
def get_object(self) -> _MT_co: ...
101
def get_queryset(self) -> QuerySet[_MT_co]: ...
102
def filter_queryset(self, queryset: QuerySet[_MT_co]) -> QuerySet[_MT_co]: ...
103
def paginate_queryset(self, queryset: QuerySet[_MT_co] | Sequence[Any]) -> Sequence[Any] | None: ...
104
def get_paginated_response(self, data: Any) -> Response: ...
105
106
# Serializer methods
107
def get_serializer(self, *args: Any, **kwargs: Any) -> BaseSerializer[_MT_co]: ...
108
def get_serializer_class(self) -> type[BaseSerializer[_MT_co]]: ...
109
def get_serializer_context(self) -> dict[str, Any]: ...
110
```
111
112
**Parameters:**
113
- `queryset: QuerySet[_MT_co] | Manager[_MT_co] | None` - Base queryset for the view
114
- `serializer_class: type[BaseSerializer[_MT_co]] | None` - Serializer class for the model
115
- `lookup_field: str` - Model field for object lookup (default: 'pk')
116
- `lookup_url_kwarg: str | None` - URL kwarg name for lookup (defaults to lookup_field)
117
118
## View Mixins
119
120
### CreateModelMixin
121
122
```python { .api }
123
class CreateModelMixin:
124
"""Mixin for creating model instances."""
125
126
def create(self, request: Request, *args: Any, **kwargs: Any) -> Response: ...
127
def perform_create(self: UsesQuerySet[_MT], serializer: BaseSerializer[_MT]) -> None: ...
128
def get_success_headers(self, data: Any) -> dict[str, str]: ...
129
```
130
131
### ListModelMixin
132
133
```python { .api }
134
class ListModelMixin:
135
"""Mixin for listing model instances."""
136
137
def list(self, request: Request, *args: Any, **kwargs: Any) -> Response: ...
138
```
139
140
### RetrieveModelMixin
141
142
```python { .api }
143
class RetrieveModelMixin:
144
"""Mixin for retrieving a model instance."""
145
146
def retrieve(self, request: Request, *args: Any, **kwargs: Any) -> Response: ...
147
```
148
149
### UpdateModelMixin
150
151
```python { .api }
152
class UpdateModelMixin:
153
"""Mixin for updating model instances."""
154
155
def update(self, request: Request, *args: Any, **kwargs: Any) -> Response: ...
156
def partial_update(self, request: Request, *args: Any, **kwargs: Any) -> Response: ...
157
def perform_update(self: UsesQuerySet[_MT], serializer: BaseSerializer[_MT]) -> None: ...
158
```
159
160
### DestroyModelMixin
161
162
```python { .api }
163
class DestroyModelMixin:
164
"""Mixin for deleting model instances."""
165
166
def destroy(self, request: Request, *args: Any, **kwargs: Any) -> Response: ...
167
def perform_destroy(self: UsesQuerySet[_MT], instance: _MT) -> None: ...
168
```
169
170
## Concrete Generic Views
171
172
### Create Views
173
174
```python { .api }
175
class CreateAPIView(CreateModelMixin, GenericAPIView[_MT_co]):
176
"""Concrete view for creating model instances."""
177
178
def post(self, request: Request, *args: Any, **kwargs: Any) -> Response: ...
179
```
180
181
### List Views
182
183
```python { .api }
184
class ListAPIView(ListModelMixin, GenericAPIView[_MT_co]):
185
"""Concrete view for listing model instances."""
186
187
def get(self, request: Request, *args: Any, **kwargs: Any) -> Response: ...
188
```
189
190
### Retrieve Views
191
192
```python { .api }
193
class RetrieveAPIView(RetrieveModelMixin, GenericAPIView[_MT_co]):
194
"""Concrete view for retrieving a model instance."""
195
196
def get(self, request: Request, *args: Any, **kwargs: Any) -> Response: ...
197
```
198
199
### Update Views
200
201
```python { .api }
202
class UpdateAPIView(UpdateModelMixin, GenericAPIView[_MT_co]):
203
"""Concrete view for updating model instances."""
204
205
def put(self, request: Request, *args: Any, **kwargs: Any) -> Response: ...
206
def patch(self, request: Request, *args: Any, **kwargs: Any) -> Response: ...
207
```
208
209
### Delete Views
210
211
```python { .api }
212
class DestroyAPIView(DestroyModelMixin, GenericAPIView[_MT_co]):
213
"""Concrete view for deleting model instances."""
214
215
def delete(self, request: Request, *args: Any, **kwargs: Any) -> Response: ...
216
```
217
218
### Combined Views
219
220
```python { .api }
221
class ListCreateAPIView(ListModelMixin, CreateModelMixin, GenericAPIView[_MT_co]):
222
"""List and create model instances."""
223
224
def get(self, request: Request, *args: Any, **kwargs: Any) -> Response: ...
225
def post(self, request: Request, *args: Any, **kwargs: Any) -> Response: ...
226
227
class RetrieveUpdateAPIView(RetrieveModelMixin, UpdateModelMixin, GenericAPIView[_MT_co]):
228
"""Retrieve and update model instances."""
229
230
def get(self, request: Request, *args: Any, **kwargs: Any) -> Response: ...
231
def put(self, request: Request, *args: Any, **kwargs: Any) -> Response: ...
232
def patch(self, request: Request, *args: Any, **kwargs: Any) -> Response: ...
233
234
class RetrieveDestroyAPIView(RetrieveModelMixin, DestroyModelMixin, GenericAPIView[_MT_co]):
235
"""Retrieve and delete model instances."""
236
237
def get(self, request: Request, *args: Any, **kwargs: Any) -> Response: ...
238
def delete(self, request: Request, *args: Any, **kwargs: Any) -> Response: ...
239
240
class RetrieveUpdateDestroyAPIView(
241
RetrieveModelMixin,
242
UpdateModelMixin,
243
DestroyModelMixin,
244
GenericAPIView[_MT_co]
245
):
246
"""Full CRUD operations for model instances."""
247
248
def get(self, request: Request, *args: Any, **kwargs: Any) -> Response: ...
249
def put(self, request: Request, *args: Any, **kwargs: Any) -> Response: ...
250
def patch(self, request: Request, *args: Any, **kwargs: Any) -> Response: ...
251
def delete(self, request: Request, *args: Any, **kwargs: Any) -> Response: ...
252
```
253
254
## ViewSet Classes
255
256
### ViewSetMixin
257
258
```python { .api }
259
class ViewSetMixin:
260
"""Base mixin that converts a ViewSet class into a concrete view."""
261
262
# Class variables assigned in as_view()
263
name: str | None
264
description: str | None
265
suffix: str | None
266
detail: bool
267
basename: str
268
# Instance attributes assigned in view wrapper
269
action_map: dict[str, str]
270
args: tuple[Any, ...]
271
kwargs: dict[str, Any]
272
# Assigned in initialize_request()
273
action: str
274
275
@classmethod
276
def as_view(
277
cls,
278
actions: dict[str, str | ViewSetAction] | None = None,
279
**initkwargs: Any
280
) -> AsView[GenericView]: ...
281
282
def initialize_request(self, request: HttpRequest, *args: Any, **kwargs: Any) -> Request: ...
283
def reverse_action(self, url_name: str, *args: Any, **kwargs: Any) -> str: ...
284
@classmethod
285
def get_extra_actions(cls) -> list[_ViewFunc]: ...
286
def get_extra_action_url_map(self) -> dict[str, str]: ...
287
```
288
289
### ViewSet
290
291
```python { .api }
292
class ViewSet(ViewSetMixin, APIView):
293
"""Base ViewSet class providing the interface for routing multiple actions."""
294
pass
295
```
296
297
### GenericViewSet
298
299
```python { .api }
300
class GenericViewSet(ViewSetMixin, GenericAPIView[_MT_co]):
301
"""Generic ViewSet with model type support."""
302
pass
303
```
304
305
### Model ViewSets
306
307
```python { .api }
308
class ReadOnlyModelViewSet(
309
RetrieveModelMixin,
310
ListModelMixin,
311
GenericViewSet[_MT_co]
312
):
313
"""ViewSet that provides read-only actions."""
314
pass
315
316
class ModelViewSet(
317
CreateModelMixin,
318
RetrieveModelMixin,
319
UpdateModelMixin,
320
DestroyModelMixin,
321
ListModelMixin,
322
GenericViewSet[_MT_co]
323
):
324
"""ViewSet that provides full CRUD actions."""
325
pass
326
```
327
328
## Usage Examples
329
330
### Custom API View
331
332
```python { .api }
333
from rest_framework.views import APIView
334
from rest_framework.response import Response
335
from rest_framework import status
336
from rest_framework.permissions import IsAuthenticated
337
338
class BookSearchView(APIView):
339
"""Custom view for searching books."""
340
341
permission_classes = [IsAuthenticated]
342
343
def get(self, request: Request) -> Response:
344
query = request.query_params.get('q', '')
345
if not query:
346
return Response(
347
{'error': 'Query parameter q is required'},
348
status=status.HTTP_400_BAD_REQUEST
349
)
350
351
books = Book.objects.filter(title__icontains=query)
352
serializer = BookSerializer(books, many=True)
353
return Response(serializer.data)
354
355
def post(self, request: Request) -> Response:
356
"""Advanced search with filters."""
357
serializer = BookSearchSerializer(data=request.data)
358
if serializer.is_valid():
359
# Perform complex search logic
360
results = self.perform_search(serializer.validated_data)
361
return Response(results)
362
return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST)
363
364
def perform_search(self, filters: dict[str, Any]) -> list[dict[str, Any]]:
365
"""Perform the actual search operation."""
366
queryset = Book.objects.all()
367
368
if 'genre' in filters:
369
queryset = queryset.filter(genre=filters['genre'])
370
if 'author' in filters:
371
queryset = queryset.filter(author__name__icontains=filters['author'])
372
if 'published_after' in filters:
373
queryset = queryset.filter(published_date__gte=filters['published_after'])
374
375
serializer = BookSerializer(queryset, many=True)
376
return serializer.data
377
```
378
379
### Generic List Create View
380
381
```python { .api }
382
from rest_framework import generics
383
from rest_framework.permissions import IsAuthenticated
384
385
class BookListCreateView(generics.ListCreateAPIView[Book]):
386
"""List books and create new books."""
387
388
queryset = Book.objects.all()
389
serializer_class = BookSerializer
390
permission_classes = [IsAuthenticated]
391
392
def get_queryset(self) -> QuerySet[Book]:
393
"""Filter books by current user if requested."""
394
queryset = super().get_queryset()
395
user_only = self.request.query_params.get('user_only', 'false')
396
397
if user_only.lower() == 'true':
398
return queryset.filter(created_by=self.request.user)
399
return queryset
400
401
def perform_create(self, serializer: BookSerializer) -> None:
402
"""Set the created_by field to current user."""
403
serializer.save(created_by=self.request.user)
404
```
405
406
### Model ViewSet
407
408
```python { .api }
409
from rest_framework import viewsets, status
410
from rest_framework.decorators import action
411
from rest_framework.response import Response
412
413
class BookViewSet(viewsets.ModelViewSet[Book]):
414
"""Full CRUD operations for books."""
415
416
queryset = Book.objects.all()
417
serializer_class = BookSerializer
418
419
def get_permissions(self) -> list[BasePermission]:
420
"""Different permissions for different actions."""
421
if self.action in ['create', 'update', 'partial_update', 'destroy']:
422
permission_classes = [IsAuthenticated]
423
else:
424
permission_classes = []
425
return [permission() for permission in permission_classes]
426
427
def get_serializer_class(self) -> type[BaseSerializer[Book]]:
428
"""Different serializers for different actions."""
429
if self.action == 'list':
430
return BookListSerializer
431
elif self.action in ['create', 'update', 'partial_update']:
432
return BookWriteSerializer
433
return BookDetailSerializer
434
435
@action(detail=True, methods=['post'])
436
def favorite(self, request: Request, pk: str | None = None) -> Response:
437
"""Add book to user's favorites."""
438
book = self.get_object()
439
user = request.user
440
441
if user in book.favorited_by.all():
442
return Response(
443
{'detail': 'Already favorited'},
444
status=status.HTTP_400_BAD_REQUEST
445
)
446
447
book.favorited_by.add(user)
448
return Response({'detail': 'Added to favorites'})
449
450
@action(detail=True, methods=['delete'])
451
def unfavorite(self, request: Request, pk: str | None = None) -> Response:
452
"""Remove book from user's favorites."""
453
book = self.get_object()
454
user = request.user
455
456
if user not in book.favorited_by.all():
457
return Response(
458
{'detail': 'Not in favorites'},
459
status=status.HTTP_400_BAD_REQUEST
460
)
461
462
book.favorited_by.remove(user)
463
return Response({'detail': 'Removed from favorites'})
464
465
@action(detail=False, methods=['get'])
466
def popular(self, request: Request) -> Response:
467
"""Get popular books based on favorites count."""
468
popular_books = Book.objects.annotate(
469
favorites_count=Count('favorited_by')
470
).order_by('-favorites_count')[:10]
471
472
serializer = self.get_serializer(popular_books, many=True)
473
return Response(serializer.data)
474
```
475
476
### Custom ViewSet Actions
477
478
```python { .api }
479
from rest_framework.decorators import action
480
481
class AuthorViewSet(viewsets.ModelViewSet[Author]):
482
queryset = Author.objects.all()
483
serializer_class = AuthorSerializer
484
485
@action(detail=True, methods=['get'])
486
def books(self, request: Request, pk: str | None = None) -> Response:
487
"""Get all books by this author."""
488
author = self.get_object()
489
books = author.book_set.all()
490
serializer = BookSerializer(books, many=True)
491
return Response(serializer.data)
492
493
@action(detail=True, methods=['get'])
494
def statistics(self, request: Request, pk: str | None = None) -> Response:
495
"""Get author statistics."""
496
author = self.get_object()
497
stats = {
498
'total_books': author.book_set.count(),
499
'average_rating': author.book_set.aggregate(
500
avg_rating=Avg('rating')
501
)['avg_rating'] or 0,
502
'latest_book': author.book_set.order_by('-published_date').first().title
503
}
504
return Response(stats)
505
506
@action(detail=False, methods=['get'], url_path='by-genre/(?P<genre>[^/.]+)')
507
def by_genre(self, request: Request, genre: str | None = None) -> Response:
508
"""Get authors who have written books in a specific genre."""
509
authors = Author.objects.filter(book__genre=genre).distinct()
510
serializer = self.get_serializer(authors, many=True)
511
return Response(serializer.data)
512
```
513
514
## View Configuration
515
516
### Permissions and Authentication
517
518
```python { .api }
519
class ProtectedBookView(generics.RetrieveUpdateDestroyAPIView[Book]):
520
queryset = Book.objects.all()
521
serializer_class = BookSerializer
522
authentication_classes = [TokenAuthentication, SessionAuthentication]
523
permission_classes = [IsAuthenticated, IsOwnerOrReadOnly]
524
525
def get_permissions(self) -> list[BasePermission]:
526
"""Custom permission logic based on action."""
527
if self.request.method in ['PUT', 'PATCH', 'DELETE']:
528
permission_classes = [IsAuthenticated, IsOwner]
529
else:
530
permission_classes = [AllowAny]
531
return [permission() for permission in permission_classes]
532
```
533
534
### Filtering and Pagination
535
536
```python { .api }
537
from rest_framework.filters import SearchFilter, OrderingFilter
538
from django_filters.rest_framework import DjangoFilterBackend
539
540
class BookListView(generics.ListAPIView[Book]):
541
queryset = Book.objects.all()
542
serializer_class = BookSerializer
543
filter_backends = [DjangoFilterBackend, SearchFilter, OrderingFilter]
544
filterset_fields = ['genre', 'author', 'published_date']
545
search_fields = ['title', 'description', 'author__name']
546
ordering_fields = ['title', 'published_date', 'rating']
547
ordering = ['-published_date']
548
pagination_class = PageNumberPagination
549
```
550
551
## Utility Functions
552
553
### View Helper Functions
554
555
```python { .api }
556
def get_view_name(view: APIView) -> str:
557
"""Get a human-readable name for the view."""
558
...
559
560
def get_view_description(view: APIView, html: bool = False) -> str:
561
"""Get a human-readable description for the view."""
562
...
563
564
def set_rollback() -> None:
565
"""Mark the current transaction as rollback-only."""
566
...
567
568
def exception_handler(exc: Exception, context: dict[str, Any]) -> Response | None:
569
"""Default exception handler for DRF views."""
570
...
571
```
572
573
## Performance Optimization
574
575
### Query Optimization
576
577
```python { .api }
578
class OptimizedBookViewSet(viewsets.ModelViewSet[Book]):
579
serializer_class = BookSerializer
580
581
def get_queryset(self) -> QuerySet[Book]:
582
"""Optimize queries with select_related and prefetch_related."""
583
return Book.objects.select_related('author', 'publisher') \
584
.prefetch_related('genres', 'reviews')
585
586
def list(self, request: Request, *args: Any, **kwargs: Any) -> Response:
587
"""Custom list method with query optimization."""
588
queryset = self.get_queryset()
589
590
# Add any additional filtering
591
search = request.query_params.get('search')
592
if search:
593
queryset = queryset.filter(
594
Q(title__icontains=search) |
595
Q(description__icontains=search)
596
)
597
598
page = self.paginate_queryset(queryset)
599
if page is not None:
600
serializer = self.get_serializer(page, many=True)
601
return self.get_paginated_response(serializer.data)
602
603
serializer = self.get_serializer(queryset, many=True)
604
return Response(serializer.data)
605
```
606
607
### Caching
608
609
```python { .api }
610
from django.core.cache import cache
611
from django.utils.decorators import method_decorator
612
from django.views.decorators.cache import cache_page
613
614
class CachedBookListView(generics.ListAPIView[Book]):
615
queryset = Book.objects.all()
616
serializer_class = BookSerializer
617
618
@method_decorator(cache_page(60 * 15)) # Cache for 15 minutes
619
def dispatch(self, *args: Any, **kwargs: Any) -> Response:
620
return super().dispatch(*args, **kwargs)
621
622
def list(self, request: Request, *args: Any, **kwargs: Any) -> Response:
623
cache_key = f"book_list_{request.GET.urlencode()}"
624
cached_response = cache.get(cache_key)
625
626
if cached_response is not None:
627
return Response(cached_response)
628
629
response = super().list(request, *args, **kwargs)
630
cache.set(cache_key, response.data, timeout=60 * 15)
631
return response
632
```
633
634
This comprehensive view and viewset system provides type-safe HTTP request handling with full generic type support, enabling confident API development with complete mypy integration.