djangorestframework review
Django REST framework, usually called DRF, builds HTTP APIs on top of Django's request handling, ORM, authentication, and settings. Serializers parse and validate request data, APIView supplies policy hooks and content negotiation, and generic views or viewsets connect those pieces to CRUD operations. Routers can derive URL patterns from a viewset. Version 3.18.0 requires Django 5.2 or newer, adds Django 6.1 support, adds accent-insensitive PostgreSQL search and a function-view throttle decorator, and changes `many=True` validation errors from a list shape to a dictionary shape. Our Python 3.12 import worked, but the package is useful only inside a configured Django project.
DRF remains the practical default for a substantial API inside Django, especially when serializer and queryset control matter. Skip it for a small standalone service, and treat a 3.18 upgrade as a client contract change if bulk validation errors cross the wire.
We installed it
| Install | ✓ · 0.8s | 4 packages on disk · 45 MB |
| Import | ✓ | import rest_framework in 0.01s · pure Python · requires Python >=3.10 |
| Known vulns | 0 | (pip-audit) |
Answers from our run
Does djangorestframework install cleanly?
Yes. In a fresh container with an empty cache, pip install djangorestframework finished in 0.8s, leaving 4 packages and 45 MB on disk. pip-audit reported no known vulnerabilities.
What does djangorestframework need to run?
Python >=3.10, and nothing compiled: it is pure Python. In our run import rest_framework succeeded in 0.01s.
djangorestframework or django-ninja: which should you use?
django-ninja: Choose it when the project stays on Django but typed function signatures and generated OpenAPI should drive endpoint definitions. DRF remains the practical default for a substantial API inside Django, especially when serializer and queryset control matter.
When should you not use djangorestframework?
The service has no reason to adopt Django. DRF 3.18.0 requires Django 5.2 or newer, so a small isolated JSON endpoint inherits Django's project and ORM conventions.
Use it if
- The API belongs to an existing Django application and should reuse its models, users, permissions, cache, and deployment setup.
- Endpoints need serializer classes with field-level and cross-field validation instead of direct model-to-JSON conversion.
- A team wants conventional CRUD views plus explicit places to scope querysets, set permissions, and control representations.
- The HTML browsable API will help developers or trusted staff inspect requests, responses, authentication, and forms.
- The service has no reason to adopt Django. DRF 3.18.0 requires Django 5.2 or newer, so a small isolated JSON endpoint inherits Django's project and ORM conventions.
- Your project remains on Django 4.2, 5.0, or 5.1. Release 3.18.0 dropped all three lines and cannot be a routine patch upgrade there.
- Async endpoint functions are the main design. DRF's APIView dispatch path is synchronous in 3.18.0, which is a poor base for handlers dominated by concurrent network waits.
- Exact quotas or attack resistance depend on application throttles. DRF documents that its cache counters use non-atomic operations and are unsuitable for brute-force or denial-of-service protection.
- You expect object permissions to filter collection responses automatically. Generic views check them when retrieving one object; list querysets still need explicit user or tenant scoping.
- Your clients depend on the pre-3.18 list shape for bulk validation errors. The 3.18 release deliberately returns those `many=True` errors as a dictionary.
Setup reality
We installed djangorestframework 3.18.0 in a fresh Python 3.12 Bookworm container. pip finished in 0.8 seconds, left 4 packages on disk, and used 45 MB. DRF declares 1 direct dependency, requires Python 3.10 or newer, and is pure Python. pip-audit found 0 known vulnerabilities. import rest_framework completed in 0.01 seconds. The package has no py.typed marker, and its measured license metadata was unknown.
Add rest_framework to INSTALLED_APPS, then define REST_FRAMEWORK defaults for authentication, permissions, pagination, and throttling. DRF does not choose a safe authorization policy for your product. SessionAuthentication follows Django's session model and requires a CSRF token for authenticated unsafe requests. Built-in TokenAuthentication also needs rest_framework.authtoken plus its database migration, and production traffic must use HTTPS. Browser CORS policy belongs in Django middleware, usually through another package.
A ModelViewSet wires list, retrieve, create, update, partial update, and destroy actions. Start with narrower generic views or ReadOnlyModelViewSet when those writes are unwanted. Scope get_queryset() by the current user or tenant before get_object() runs. Object-level permission checks do not trim list responses. Serializer relations can trigger an N+1 query for every row; DRF's guide tells you to add select_related() or prefetch_related() to the queryset.
Pagination, filter fields, ordering columns, and request-size limits need deliberate caps. Built-in throttles share Django's cache and use non-atomic counters, so concurrent requests can exceed a configured rate. DRF 3.18.0 also changes bulk serializer error bodies to dictionaries. Run contract tests against invalid many=True payloads before upgrading clients. The new UnaccentedSearchFilter works only with PostgreSQL, its unaccent extension, and django.contrib.postgres in INSTALLED_APPS.
Patterns
Expose selected model fields define-model-serializer
from rest_framework import serializers
from .models import Invoice
class InvoiceSerializer(serializers.ModelSerializer):
class Meta:
model = Invoice
fields = ['id', 'number', 'status', 'total', 'created_at']
read_only_fields = ['id', 'status', 'created_at']An explicit fields list prevents a later model field from appearing in the API by accident.
Validate one field and the full payload validate-fields
class BookingSerializer(serializers.Serializer):
seats = serializers.IntegerField(min_value=1)
starts_at = serializers.DateTimeField()
ends_at = serializers.DateTimeField()
def validate(self, attrs):
if attrs['ends_at'] <= attrs['starts_at']:
raise serializers.ValidationError({
'ends_at': 'Must be later than starts_at.'
})
return attrsFor partial updates, attrs may omit fields. Merge with values from self.instance before enforcing a rule that spans old and new data.
Keep each user inside their queryset scope-viewset-queryset
from rest_framework import permissions, viewsets
class InvoiceViewSet(viewsets.ModelViewSet):
serializer_class = InvoiceSerializer
permission_classes = [permissions.IsAuthenticated]
def get_queryset(self):
return (Invoice.objects
.filter(account=self.request.user.account)
.select_related('account'))
def perform_create(self, serializer):
serializer.save(account=self.request.user.account)List views do not apply object permissions to every row. Filter by tenant in get_queryset() and assign that tenant on creation.
Mount router-generated URLs register-viewset-routes
from django.urls import include, path
from rest_framework.routers import DefaultRouter
router = DefaultRouter()
router.register('invoices', InvoiceViewSet, basename='invoice')
urlpatterns = [path('api/', include(router.urls))]ModelViewSet generates mutation and delete routes. Use ReadOnlyModelViewSet when GET is the full contract.
Build a small function endpoint write-function-view
from rest_framework.decorators import api_view, permission_classes
from rest_framework.permissions import AllowAny
from rest_framework.response import Response
@api_view(['GET'])
@permission_classes([AllowAny])
def health(request):
return Response({'status': 'ok'})DRF checks that policy decorators appear below api_view so they are applied to the generated view class.
Offer list and create only use-narrow-generic-view
from rest_framework import generics, permissions
class InvoiceList(generics.ListCreateAPIView):
serializer_class = InvoiceSerializer
permission_classes = [permissions.IsAuthenticated]
def get_queryset(self):
return Invoice.objects.filter(
account=self.request.user.account
).order_by('-created_at')ListCreateAPIView accepts GET and POST. It avoids exposing update and delete actions that this URL does not need.
Add an action with its own policy add-custom-action
from rest_framework.decorators import action
from rest_framework.permissions import IsAdminUser
from rest_framework.response import Response
class InvoiceViewSet(viewsets.ModelViewSet):
@action(detail=True, methods=['post'],
permission_classes=[IsAdminUser])
def void(self, request, pk=None):
invoice = self.get_object()
invoice.void()
return Response({'status': invoice.status})get_object() applies queryset scoping and object checks. A direct Invoice.objects.get() can bypass both.
Allowlist filters and ordering configure-filtering
from django_filters.rest_framework import DjangoFilterBackend
from rest_framework.filters import OrderingFilter, SearchFilter
class InvoiceViewSet(viewsets.ModelViewSet):
filter_backends = [DjangoFilterBackend, SearchFilter, OrderingFilter]
filterset_fields = ['status']
search_fields = ['number', 'customer__name']
ordering_fields = ['created_at', 'total']
ordering = ['-created_at']DjangoFilterBackend comes from the separate django-filter package. Restrict ordering fields to indexed, non-sensitive columns.
Cap client-selected page size configure-pagination
from rest_framework.pagination import PageNumberPagination
class InvoicePagination(PageNumberPagination):
page_size = 50
page_size_query_param = 'page_size'
max_page_size = 200
class InvoiceViewSet(viewsets.ModelViewSet):
pagination_class = InvoicePaginationmax_page_size matters only when page_size_query_param lets a client request a different size.
Require authentication by default set-global-policies
REST_FRAMEWORK = {
'DEFAULT_AUTHENTICATION_CLASSES': [
'rest_framework.authentication.SessionAuthentication',
],
'DEFAULT_PERMISSION_CLASSES': [
'rest_framework.permissions.IsAuthenticated',
],
'DEFAULT_PAGINATION_CLASS': (
'rest_framework.pagination.PageNumberPagination'
),
'PAGE_SIZE': 50,
}Authenticated session requests need CSRF tokens for POST, PUT, PATCH, and DELETE. Mark public views with AllowAny explicitly.
Apply a named rate to a function view throttle-function-view
from rest_framework.decorators import (
api_view, throttle_classes, throttle_scope
)
from rest_framework.throttling import ScopedRateThrottle
@api_view(['POST'])
@throttle_classes([ScopedRateThrottle])
@throttle_scope('exports')
def create_export(request):
...
REST_FRAMEWORK = {
'DEFAULT_THROTTLE_RATES': {'exports': '10/hour'},
}throttle_scope is new in 3.18 for function views. Cache races make this an application policy limit, not attack protection.
Test a scoped collection response test-authenticated-api
from rest_framework.test import APITestCase
class InvoiceApiTests(APITestCase):
def test_list_contains_only_account_rows(self):
self.client.force_authenticate(user=self.user)
response = self.client.get('/api/invoices/')
self.assertEqual(response.status_code, 200)
ids = [row['id'] for row in response.data['results']]
self.assertEqual(ids, [self.invoice.id])force_authenticate bypasses the real login or token path. Add separate tests for headers, session cookies, CSRF, and failed credentials.
Alternatives
| Package | Registry | Pick it when |
|---|---|---|
| django-ninja | PyPI | Choose it when the project stays on Django but typed function signatures and generated OpenAPI should drive endpoint definitions. |
| django-tastypie | PyPI | Consider it for an established Tastypie codebase whose Resource API would be costly to replace; it is rarely the first pick for a new Django API. |
| fastapi | PyPI | Choose it for a standalone ASGI service where async handlers and type-derived request schemas matter more than Django integration. |
More web backend guides
urllib3 · requests · ws · anyio · httpx · undici · the whole shelf →
How this guide is made: grounded in the library's documentation, release notes, changelog, and issue history, on a fixed rubric — not a hands-on install of every release. The 50 most-downloaded entries are additionally install-verified in clean containers. Corrections: contact the desk.

