Introduction
Django REST Framework is one of the most productive tools in the Python ecosystem. But most tutorials stop at CRUD. Here are the patterns I reach for when building healthcare and agri-tech APIs in production.
1. Custom Serializer Mixins for DRY Validation
class TimestampedSerializer(serializers.ModelSerializer):
created_at = serializers.DateTimeField(read_only=True, format="%Y-%m-%dT%H:%M:%SZ")
updated_at = serializers.DateTimeField(read_only=True, format="%Y-%m-%dT%H:%M:%SZ")
class AuditedCreateMixin(serializers.Serializer):
def create(self, validated_data):
validated_data['created_by'] = self.context['request'].user
return super().create(validated_data)
2. select_related / prefetch_related — Always
The N+1 query is the most common performance killer in DRF. Use Django Debug Toolbar in development and annotate your querysets:
class PatientViewSet(viewsets.ModelViewSet):
def get_queryset(self):
return (
Patient.objects
.select_related('facility', 'assigned_clinician')
.prefetch_related('appointments__prescriptions')
.filter(facility=self.request.user.facility)
)
3. Celery for Background Tasks
Any operation that takes > 200ms doesn't belong in a request/response cycle. Billing, notifications, report generation — all go to Celery:
@shared_task(bind=True, max_retries=3, default_retry_delay=60)
def send_appointment_reminder(self, appointment_id: int):
try:
appointment = Appointment.objects.select_related('patient').get(id=appointment_id)
SMSGateway.send(appointment.patient.phone, f"Reminder: {appointment.time}")
except Exception as exc:
raise self.retry(exc=exc)
4. Custom Pagination with Metadata
Standard PageNumberPagination gives you next and previous. Add total counts and page metadata so frontend engineers don't have to guess:
class StandardPagination(PageNumberPagination):
page_size = 20
page_size_query_param = 'page_size'
def get_paginated_response(self, data):
return Response({
'meta': {
'count': self.page.paginator.count,
'total_pages': self.page.paginator.num_pages,
'current_page': self.page.number,
},
'results': data,
})
5. PostgreSQL-Specific Optimisations
- Use
django.contrib.postgres.indexes.GinIndexfor full-text search fields - Use
F()expressions for atomic counter updates to avoid race conditions - Use
bulk_create(update_conflicts=True)for upserts instead of looping saves
# Atomic increment — no race condition
Patient.objects.filter(id=patient_id).update(visit_count=F('visit_count') + 1)
Closing Thoughts
DRF rewards you when you know Django's ORM deeply. The framework gets out of your way once you stop fighting it with custom views for everything — lean into ViewSets, mixins, and the permission/throttle system, and your APIs will scale cleanly.