Created: January 23, 2026
Last Updated: January 23, 2026
Status: Example Project
Backend:
- Python 3.11
- Django 4.2
- Django REST Framework 3.14
- PostgreSQL 15
Testing:
- pytest 7.4
- pytest-django 4.5
- factory-boy 3.3 (test data generation)
Infrastructure:
- Docker & Docker Compose
- nginx (production)
- Gunicorn (WSGI server)
Key Dependencies:
- python-dotenv (environment variables)
- psycopg2-binary (PostgreSQL adapter)
- django-cors-headers (CORS handling)
ALWAYS run these commands before claiming work is complete:
| Changed | Command | Expected Result |
|---|---|---|
| Backend Python | docker-compose exec backend pytest -x |
All tests pass, no failures |
| Backend Specific | docker-compose exec backend pytest path/to/test_file.py -x |
Specific tests pass |
| Database Migrations | docker-compose exec backend python manage.py migrate |
No migration errors |
| Code Quality | docker-compose exec backend flake8 . |
No linting errors |
Stop on first failure (-x flag) to prevent cascading errors.
Pattern: ViewSet + Serializer + Router
# blog/serializers.py
from rest_framework import serializers
from .models import Article
class ArticleSerializer(serializers.ModelSerializer):
author_name = serializers.CharField(source='author.username', read_only=True)
class Meta:
model = Article
fields = ['id', 'title', 'content', 'author', 'author_name',
'created_at', 'updated_at', 'published']
read_only_fields = ['author', 'created_at', 'updated_at']
# blog/views.py
from rest_framework import viewsets, permissions
from .models import Article
from .serializers import ArticleSerializer
class ArticleViewSet(viewsets.ModelViewSet):
queryset = Article.objects.select_related('author')
serializer_class = ArticleSerializer
permission_classes = [permissions.IsAuthenticatedOrReadOnly]
def perform_create(self, serializer):
serializer.save(author=self.request.user)
# blog/urls.py
from rest_framework.routers import DefaultRouter
from .views import ArticleViewSet
router = DefaultRouter()
router.register(r'articles', ArticleViewSet)
urlpatterns = router.urlsWhy:
- ViewSets reduce boilerplate for CRUD operations
- Serializers handle validation + representation
- Routers auto-generate URL patterns
select_relatedprevents N+1 queries
Pattern: Timestamps + Author Tracking
# blog/models.py
from django.db import models
from django.contrib.auth.models import User
class TimestampedModel(models.Model):
"""Abstract base class with timestamps"""
created_at = models.DateTimeField(auto_now_add=True)
updated_at = models.DateTimeField(auto_now=True)
class Meta:
abstract = True
class Article(TimestampedModel):
title = models.CharField(max_length=200)
content = models.TextField()
author = models.ForeignKey(User, on_delete=models.CASCADE, related_name='articles')
published = models.BooleanField(default=False)
class Meta:
ordering = ['-created_at']
def __str__(self):
return self.titleWhy:
- Abstract base class enforces timestamp consistency
related_namemakes reverse queries readableorderingprovides default sort behavior__str__improves admin interface and debugging
Pattern: factory-boy for Test Data
# blog/factories.py
import factory
from django.contrib.auth.models import User
from .models import Article
class UserFactory(factory.django.DjangoModelFactory):
class Meta:
model = User
username = factory.Sequence(lambda n: f'user{n}')
email = factory.LazyAttribute(lambda obj: f'{obj.username}@example.com')
class ArticleFactory(factory.django.DjangoModelFactory):
class Meta:
model = Article
title = factory.Faker('sentence', nb_words=5)
content = factory.Faker('paragraph', nb_sentences=10)
author = factory.SubFactory(UserFactory)
published = True
# blog/tests/test_articles.py
import pytest
from rest_framework.test import APIClient
from blog.factories import ArticleFactory, UserFactory
@pytest.mark.django_db
class TestArticleAPI:
def test_list_articles(self):
# Arrange
ArticleFactory.create_batch(3, published=True)
client = APIClient()
# Act
response = client.get('/api/articles/')
# Assert
assert response.status_code == 200
assert len(response.json()) == 3
def test_create_article_requires_auth(self):
# Arrange
client = APIClient()
data = {'title': 'Test', 'content': 'Content'}
# Act
response = client.post('/api/articles/', data)
# Assert
assert response.status_code == 401
def test_create_article_authenticated(self):
# Arrange
user = UserFactory()
client = APIClient()
client.force_authenticate(user=user)
data = {'title': 'Test', 'content': 'Content', 'published': True}
# Act
response = client.post('/api/articles/', data)
# Assert
assert response.status_code == 201
assert response.json()['author_name'] == user.usernameWhy:
- Factories eliminate test data boilerplate
Fakerprovides realistic test data- Arrange-Act-Assert structure makes tests readable
@pytest.mark.django_dbhandles database transactions
Pattern: django-environ + .env
# settings.py
import environ
env = environ.Env(
DEBUG=(bool, False),
ALLOWED_HOSTS=(list, []),
)
# Read .env file
environ.Env.read_env()
DEBUG = env('DEBUG')
SECRET_KEY = env('SECRET_KEY')
DATABASES = {
'default': env.db() # Reads DATABASE_URL
}
ALLOWED_HOSTS = env.list('ALLOWED_HOSTS')# .env.example
DEBUG=True
SECRET_KEY=your-secret-key-here
DATABASE_URL=postgresql://user:password@localhost:5432/dbname
ALLOWED_HOSTS=localhost,127.0.0.1Why:
- Environment variables for deployment flexibility
.env.exampledocuments required settings- Type casting (bool, list, db) reduces errors
- Defaults for non-critical settings
-
Always use transactions for multi-model operations
from django.db import transaction @transaction.atomic def create_article_with_tags(title, content, tag_names): article = Article.objects.create(title=title, content=content) for name in tag_names: tag, _ = Tag.objects.get_or_create(name=name) article.tags.add(tag) return article
-
Never expose DEBUG=True in production
- Check
settings.pyensuresDEBUG=Falsein production - Use environment variables, never hardcode
- Check
-
Always validate permissions in ViewSets
- Default:
permission_classes = [permissions.IsAuthenticated] - Public endpoints: explicitly set
AllowAny - Custom permissions for complex logic
- Default:
-
Never commit secrets to version control
- Use
.envfor secrets .gitignoremust include.env- Use
.env.examplefor documentation
- Use
-
Always use
select_related/prefetch_relatedfor relations- Prevents N+1 query problems
- Check Django Debug Toolbar for query counts
Problem: Multiple developers create migrations simultaneously.
Solution:
# Always pull latest migrations before creating new ones
git pull
python manage.py makemigrations
python manage.py migrate
# If conflict occurs:
python manage.py makemigrations --mergeProblem: Tests fail when run together but pass individually.
Solution:
# Use pytest fixtures with autouse for cleanup
@pytest.fixture(autouse=True)
def reset_sequences(db):
"""Reset database sequences after each test"""
from django.core.management import call_command
yield
call_command('flush', '--no-input')
# Or use pytest-django's transactional tests
@pytest.mark.django_db(transaction=True)
def test_with_transaction():
passProblem: Frontend on localhost:5173 can't access API on localhost:8000.
Solution:
# settings.py
INSTALLED_APPS = [
'corsheaders',
# ...
]
MIDDLEWARE = [
'corsheaders.middleware.CorsMiddleware',
'django.middleware.common.CommonMiddleware',
# ...
]
# Development only
if DEBUG:
CORS_ALLOWED_ORIGINS = [
"http://localhost:5173",
"http://127.0.0.1:5173",
]Problem: Static files (CSS, JS) not served in production.
Solution:
# settings.py
STATIC_URL = '/static/'
STATIC_ROOT = BASE_DIR / 'staticfiles'
# Before deployment:
python manage.py collectstatic --no-input
# nginx.conf
location /static/ {
alias /app/staticfiles/;
}Decision: Use DRF for API instead of plain Django views.
Rationale:
- Serialization + validation in one place
- Built-in pagination, filtering, authentication
- OpenAPI schema generation (drf-spectacular)
- Industry standard for Django APIs
Alternatives considered:
- Plain Django views: Too much boilerplate
- FastAPI: Would require leaving Django ecosystem
Decision: Use PostgreSQL as primary database.
Rationale:
- JSON fields for flexible data
- Full-text search capabilities
- Battle-tested in production
- Django has excellent PostgreSQL support
Alternatives considered:
- SQLite: Not suitable for production
- MySQL: PostgreSQL has better Django integration
Decision: Use pytest for all tests.
Rationale:
- More readable assertions (
assert x == yvsself.assertEqual) - Powerful fixtures for test setup
- Better parameterization support
- Active ecosystem (pytest-django, pytest-cov)
Alternatives considered:
- Django's unittest: More verbose, less flexible
backend/
├── manage.py
├── pyproject.toml # Dependencies
├── pytest.ini # Test configuration
├── project/
│ ├── settings.py # Django settings
│ ├── urls.py # Root URL config
│ └── wsgi.py # WSGI entry point
├── blog/
│ ├── models.py # Database models
│ ├── serializers.py # DRF serializers
│ ├── views.py # API views
│ ├── urls.py # App URL config
│ ├── admin.py # Django admin config
│ ├── factories.py # Test data factories
│ └── tests/
│ ├── test_models.py
│ ├── test_serializers.py
│ └── test_views.py
└── staticfiles/ # Collected static files
Conventions:
- One app per domain concept (
blog,users,comments) factories.pyalongside models for test datatests/directory with descriptive filenames- Keep
views.pylean - extract business logic toservices.pyif needed
# 1. Start services
docker-compose up -d
# 2. Apply migrations
docker-compose exec backend python manage.py migrate
# 3. Create superuser (first time only)
docker-compose exec backend python manage.py createsuperuser
# 4. Run development server (if not using Docker)
python manage.py runserver# 1. Create/modify models
# 2. Generate migration
docker-compose exec backend python manage.py makemigrations
# 3. Apply migration
docker-compose exec backend python manage.py migrate
# 4. Write tests
# 5. Run tests
docker-compose exec backend pytest -x
# 6. Commit only after tests pass# Run full test suite
docker-compose exec backend pytest
# Check code quality
docker-compose exec backend flake8 .
# Verify migrations
docker-compose exec backend python manage.py makemigrations --check --dry-run-
DEBUG=Falsein environment -
SECRET_KEYfrom secure source -
ALLOWED_HOSTSconfigured - Database backed up
- Static files collected (
collectstatic) - Migrations applied
- Gunicorn/uWSGI configured
- nginx reverse proxy configured
- HTTPS enabled
- Environment variables secured
- Monitoring configured (Sentry, logs)
# Production .env (example)
DEBUG=False
SECRET_KEY=<random-50-char-string>
DATABASE_URL=postgresql://user:password@db:5432/production_db
ALLOWED_HOSTS=example.com,www.example.com
CORS_ALLOWED_ORIGINS=https://example.comThis example demonstrates a production-ready Django REST API with best practices for models, serializers, testing, and deployment.