Django Conventions
Fat Models, Thin Views
- Place business logic in models and custom managers — views should only dispatch; testable logic should not depend on HTTP
- Use model methods for operations on a single instance
- Use custom managers and QuerySet methods for operations on collections
- Keep views under 20 lines — extract logic into services or model methods
# models.py — business logic lives here
class ArticleQuerySet(models.QuerySet):
def published(self):
return self.filter(status="published", publish_date__lte=timezone.now())
def by_author(self, user):
return self.filter(author=user)
class Article(models.Model):
title = models.CharField(max_length=200)
status = models.CharField(max_length=20, default="draft")
publish_date = models.DateTimeField(null=True, blank=True)
author = models.ForeignKey("auth.User", on_delete=models.CASCADE)
objects = ArticleQuerySet.as_manager()
def publish(self):
self.status = "published"
self.publish_date = timezone.now()
self.save(update_fields=["status", "publish_date"])
QuerySet Optimization
- Use
select_related() for ForeignKey and OneToOne fields accessed in the same request
- Use
prefetch_related() for ManyToMany and reverse ForeignKey lookups
- Use
only() or defer() to limit columns when full model loading is unnecessary
- Use
iterator() for large querysets that do not need caching — avoids loading all rows into memory
- Never evaluate querysets in templates with additional queries — prefetch in the view
# BAD: N+1 queries — each article.author triggers a separate query
articles = Article.objects.all()
# GOOD: single JOIN, no N+1
articles = Article.objects.select_related("author").published()
Async Views & ORM
- Use
async def views for I/O-bound endpoints — async views avoid thread-pool exhaustion under high concurrency
- Use async ORM methods (
aget(), afilter(), acreate(), acount()) in async views — mixing sync ORM in async views triggers sync_to_async overhead
- Ensure all middleware supports async — a single sync middleware forces Django to use a thread per request
- Deploy with Uvicorn or Hypercorn for ASGI — required for async views to deliver concurrency benefits
GeneratedField (Django 5.0+)
- Use
GeneratedField for database-computed columns instead of Python @property — values are queryable, indexable, and computed at the database level
- Choose
db_persist=True (stored) for frequently read values, db_persist=False (virtual) for rarely accessed computed values
Security Defaults
- Use
LoginRequiredMiddleware (Django 5.1+) to require authentication by default — decorate public views with @login_not_required
- This inverts the security model from opt-in to opt-out — prevents accidentally exposing views
DRF Serializers
- Use
validate_<field>() methods for field-level validation
- Use
validate() for cross-field validation
- Keep serializers focused — use separate serializers for list vs detail
- Use
SerializerMethodField sparingly — prefer annotated querysets for computed fields
- Consider Django Ninja for new API projects — Pydantic-based validation, async-first, auto-generated OpenAPI docs
Migrations
- Use
django.db.migrations for all schema changes — never write manual SQL for schema
- Run
makemigrations and migrate in development before pushing
- Review generated migrations — squash when the chain grows beyond 10 per app
- Use
RunPython with a reverse function for data migrations
Transactions
- Wrap multi-step database operations in
@transaction.atomic — partial writes leave data in an inconsistent state
- Use
select_for_update() when concurrent writes to the same rows are possible — prevents lost updates
- Keep transactions short — do not include external API calls inside atomic blocks; slow calls hold locks and block other requests
Settings
- Split settings into
base.py, dev.py, and prod.py
- Load secrets from environment variables using
os.environ.get() or django-environ
- Never commit
.env files — add them to .gitignore
Testing
- Use
pytest-django with the @pytest.mark.django_db marker
- Use factories (
factory_boy) instead of fixtures for test data
- Test views via the Django test client or DRF’s
APIClient
- Isolate tests — each test must create its own data and clean up