Opinionated coding, performance, and security rules for Python 3.13 projects that use Django 5.2 and Django REST Framework 3.x to build production-grade REST APIs.
Building production-grade REST APIs with Django shouldn't mean spending hours tracking down N+1 queries, debugging serialization bottlenecks, or wrestling with deployment configurations. Yet most Django REST Framework projects end up exactly there—drowning in performance issues, security gaps, and inconsistent code patterns that slow down your entire team.
Every Django REST Framework project faces the same predictable challenges:
These aren't Django problems—they're consistency problems. When every developer makes different architectural decisions, your codebase becomes a maintenance nightmare.
This comprehensive ruleset eliminates the guesswork from Django REST Framework development. Instead of debating patterns in code reviews, you get battle-tested conventions that solve real production problems:
# Before: N+1 query disaster
class UserViewSet(viewsets.ModelViewSet):
queryset = User.objects.all()
# After: Optimized from day one
class UserViewSet(viewsets.ModelViewSet):
queryset = User.objects.select_related('profile').prefetch_related('permissions')
serializer_class = UserSerializer
permission_classes = [IsAuthenticated]
pagination_class = StandardResultsSetPagination
Eliminate Performance Debugging Sessions
select_related and prefetch_related patterns prevent N+1 queries before they happenSecurity by Default, Not by Accident
djangorestframework-simplejwt configured consistently across all endpointsType-Safe Development with Immediate Feedback
mypy --strict configuration eliminates runtime type errorsTesting That Actually Prevents Bugs
Before: Spending hours with Django Debug Toolbar tracking down why your user list endpoint makes 127 database queries:
# This innocent-looking code creates a performance disaster
def get_users_with_profiles():
users = User.objects.all()
return [
{
'name': user.name,
'email': user.email,
'profile_bio': user.profile.bio, # N+1 query here
'post_count': user.posts.count() # Another N+1 here
}
for user in users
]
After: Query optimization is automatic with enforced patterns:
class UserViewSet(viewsets.ModelViewSet):
queryset = User.objects.select_related('profile').prefetch_related('posts')
@action(detail=False, methods=['get'])
def with_stats(self, request):
users = self.get_queryset().annotate(
post_count=Count('posts')
)
serializer = UserStatsSerializer(users, many=True)
return Response(serializer.data)
Result: 127 queries become 3 queries. Your endpoint goes from 2.3 seconds to 89ms.
Before: Different endpoints handle validation errors differently, creating inconsistent API responses:
# Endpoint A returns this format
{"error": "Invalid email"}
# Endpoint B returns this format
{"detail": "Email validation failed", "field": "email"}
# Endpoint C just returns 500 errors
After: Standardized error handling across all endpoints:
class PaymentSerializer(serializers.ModelSerializer):
def validate_amount(self, value):
if value <= 0:
raise serializers.ValidationError(
"Amount must be greater than zero",
code="invalid_amount"
)
return value
def validate(self, attrs):
try:
payment_processor.validate_payment(attrs)
except PaymentDeclined as exc:
raise serializers.ValidationError(str(exc))
return attrs
Every validation error now returns the same format: {"detail": "string", "code": "error_code"}.
Before: Your staging environment works, but production fails with mysterious configuration issues:
# Inconsistent environment handling
FROM python:3.13
COPY . .
RUN pip install -r requirements.txt
# Missing security headers, wrong ASGI config, no health checks
After: Production-ready deployment from day one:
FROM python:3.13-slim
ENV PYTHONDONTWRITEBYTECODE 1
ENV PYTHONUNBUFFERED 1
RUN apt-get update && apt-get install -y netcat gcc postgresql-client && rm -rf /var/lib/apt/lists/*
WORKDIR /srv/app
COPY requirements.txt .
RUN pip install --no-cache-dir -r requirements.txt
COPY . .
CMD ["gunicorn", "config.asgi:application", "-k", "uvicorn.workers.UvicornWorker", "--bind", "0.0.0.0:8000"]
Plus mandatory health check endpoint, environment variable validation, and ASGI optimization.
pip install django djangorestframework djangorestframework-simplejwt
pip install django-cors-headers django-filter django-cacheops
pip install celery redis psycopg2-binary
pip install pytest pytest-django factory-boy black isort ruff mypy
# settings.py
REST_FRAMEWORK = {
'DEFAULT_AUTHENTICATION_CLASSES': [
'rest_framework_simplejwt.authentication.JWTAuthentication',
],
'DEFAULT_PERMISSION_CLASSES': [
'rest_framework.permissions.IsAuthenticated',
],
'DEFAULT_PAGINATION_CLASS': 'rest_framework.pagination.PageNumberPagination',
'PAGE_SIZE': 50
}
# Security defaults
SECURE_HSTS_SECONDS = 31536000
SECURE_SSL_REDIRECT = True
CORS_ALLOWED_ORIGINS = os.getenv('CORS_ALLOWED_ORIGINS', '').split(',')
project_root/
├── config/ # settings, ASGI/WSGI, URLs
├── apps/
│ ├── users/
│ │ ├── models.py
│ │ ├── serializers.py
│ │ ├── views.py
│ │ ├── urls.py
│ │ └── tests/
│ └── payments/
├── requirements/ # base, dev, prod
├── docker/
└── scripts/
Every ViewSet follows this template:
class UserViewSet(viewsets.ModelViewSet):
queryset = User.objects.select_related('profile').prefetch_related('groups')
serializer_class = UserSerializer
permission_classes = [IsAuthenticated]
pagination_class = StandardResultsSetPagination
filter_backends = [DjangoFilterBackend, SearchFilter]
filterset_fields = ['is_active', 'created_at']
search_fields = ['username', 'email']
# pytest.ini
[tool:pytest]
DJANGO_SETTINGS_MODULE = config.settings.test
python_files = tests.py test_*.py *_tests.py
addopts = --cov=apps --cov-report=html --cov-fail-under=90
Performance Improvements:
Development Velocity:
Team Consistency:
Stop debugging the same performance and security issues across every Django REST Framework project. These rules transform your development workflow from reactive problem-solving to proactive, consistent API development.
Your production APIs deserve better than ad-hoc patterns and emergency debugging sessions. Implement these rules and build Django REST Framework applications that scale, perform, and maintain themselves.
You are an expert in Python 3.13, Django 5.2, Django REST Framework 3.15, PostgreSQL 15, Redis 7, Celery 5, Docker, and ASGI servers (Uvicorn/Daphne).
Key Principles
- Ship deterministic, repeatable builds; keep all config in environment variables (12-Factor).
- Favour Django ORM over raw SQL; eliminate N+1 queries with `select_related` / `prefetch_related`.
- Embrace type-safety: annotate everything and run `mypy --strict` in CI.
- Keep functions small (<40 lines) and pure where possible; prefer early returns.
- Write idempotent APIs; every endpoint must be stateless, paginated, and versioned.
- Secure by default: never log secrets, always validate user input, escape all dynamic output.
Python
- Use `black` + `isort` + `ruff` for formatting/linting; no manual style tweaks.
- File naming: `snake_case.py`; classes `PascalCase`; functions/vars `snake_case`; bools prefixed with `is_/has_/can_`.
- Prefer dataclasses over mutable dictionaries for in-memory data objects.
- Use f-strings; forbid `%` or `str.format` for new code.
- Async rules:
- `async def` only when the entire call-chain is async-aware.
- Await I/O exclusively (`database_sync_to_async`, HTTP calls, S3, etc.).
- Never run CPU-bound work in the event-loop; off-load to Celery or `concurrent.futures`.
- Context management:
```python
with connection.cursor() as cursor:
cursor.execute(...)
```
Required; never forget `close()` calls.
Error Handling & Validation
- Validate request data in DRF `Serializer.validate_*` or `validate()` methods.
- Catch domain-specific exceptions and raise appropriate DRF exceptions:
```python
try:
payment.charge()
except PaymentDeclined as exc:
raise exceptions.ValidationError(str(exc))
```
- Always return DRF `Response`; never return raw `HttpResponse` from API endpoints.
- Provide default error structure:
```json
{"detail": "string", "code": "error_code"}
```
- Log unexpected exceptions via `logging.getLogger(__name__)` and let DRF’s exception handler format the response.
Django REST Framework
- ViewSets:
- Define `queryset`, `serializer_class`, `permission_classes`, `pagination_class`.
- Custom endpoints use `@action(detail=..., methods=["get", "post"], url_path="...")`.
- Use `SelectRelatedMixin` / `PrefetchRelatedMixin` (from `drf_serializer_extensions`) to auto-optimise queries.
- Serializers:
- `class Meta: model = …; fields = "__all__"` forbidden in public APIs; enumerate explicit fields.
- Mark read-only DB fields with `read_only_fields`.
- Use `SerializerMethodField` for computed values; cache heavy computations.
- Authentication & Permissions:
- Prefer JWT (`djangorestframework-simplejwt`).
- Set `REST_FRAMEWORK["DEFAULT_AUTHENTICATION_CLASSES"] = ("rest_framework_simplejwt.authentication.JWTAuthentication",)`.
- Custom permissions derive from `BasePermission`; keep them pure and side-effect free.
- Pagination:
- Default: `PageNumberPagination` with `page_size=50`; allow override via `page_size_query_param` max = 200.
- Filtering & Searching:
- Use `django-filters` `DjangoFilterBackend` for simple filters.
- Text search via PostgreSQL `SearchVector` + `TrigramSimilarity`; expose `/search/?q=`.
Testing
- Use `pytest` + `pytest-django`; factory data with `factory_boy`.
- Minimum 90 % branch coverage; fail CI below that threshold.
- Each bug requires a regression test reproducing the issue.
- Integration tests spin up Postgres & Redis with Docker Compose.
Performance
- Always inspect SQL via Django Debug Toolbar in local dev.
- Add indexes for frequently filtered columns (`index_together`, `db_index`).
- Query limits:
```python
qs = User.objects.only("id", "email")[:100]
```
- Cache expensive serializer output in Redis (`django-cacheops` or DRF’s `@cache_response`).
- Deploy with ASGI + `--workers 4 --max-requests 2000` (Gunicorn/uvicorn workers tuned by CPU cores).
Security
- CSRF enabled for cookie-auth endpoints; for JWT set `CSRF_TRUSTED_ORIGINS`.
- Use `django-cors-headers`, allowlist origins only.
- All secrets pulled from env vars; never commit `.env` to VCS.
- Use `SECURE_HSTS_SECONDS = 31536000`, `SECURE_SSL_REDIRECT = True` in production.
- Deny unsafe HTTP methods by default; require explicit `@action(methods=[...])`.
Deployment
- Docker image:
```dockerfile
FROM python:3.13-slim
ENV PYTHONDONTWRITEBYTECODE 1
ENV PYTHONUNBUFFERED 1
RUN apt-get update && apt-get install -y netcat gcc postgresql-client && rm -rf /var/lib/apt/lists/*
WORKDIR /srv/app
COPY requirements.txt .
RUN pip install --no-cache-dir -r requirements.txt
COPY . .
CMD ["gunicorn", "config.asgi:application", "-k", "uvicorn.workers.UvicornWorker", "--bind", "0.0.0.0:8000"]
```
- Health-check endpoint at `/health/` returns DB & cache status.
Additional Utilities
- Background tasks via Celery + Redis; retry policy `max_retries=3, backoff=2`.
- Migrations: `python manage.py makemigrations --check --dry-run` in CI to prevent missing migrations.
- Admin hardening: whitelist IPs, use two-factor auth plugin.
Directory Layout
```
project_root/
├── config/ # settings, ASGI/WGI, URLs, routing
├── apps/
│ ├── users/
│ │ ├── models.py # business logic only, no prints/logging
│ │ ├── serializers.py
│ │ ├── views.py
│ │ ├── urls.py
│ │ └── tests/
│ └── payments/
├── requirements/ # base, dev, prod txt files
├── docker/
└── scripts/
```
Common Pitfalls
- Forgetting to prefetch related objects ➔ leads to N+1; always profile with toolbar.
- Returning plain dict instead of `Response` ➔ triggers 500 in DRF.
- Failing to call `is_valid(raise_exception=True)` ➔ hidden validation errors.
- Missing index on `created_at` filters ➔ full table scan.
Use these rules verbatim to keep Python + Django REST Framework projects clean, fast, and secure.