Homechevron_rightBlogchevron_rightBackend
Backendschedule7 min read10 April 2025

Django REST Framework: Patterns I Use in Production

From custom serializer mixins to Celery task patterns and PostgreSQL query optimisation — the DRF techniques that actually matter when your API handles real traffic.

PythonDjangoPostgreSQLRESTCelery

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.GinIndex for 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.