The Django patterns you need every day — models, ORM queries, views, serializers, authentication, and DRF — with copy-ready code in every section.
Quick reference
| Task | Code |
|---|---|
| Create project | django-admin startproject mysite |
| Create app | python manage.py startapp blog |
| Run dev server | python manage.py runserver |
| Make migrations | python manage.py makemigrations |
| Apply migrations | python manage.py migrate |
| Create superuser | python manage.py createsuperuser |
| Open Django shell | python manage.py shell |
| Run tests | python manage.py test |
| Collect static files | python manage.py collectstatic |
| Dump data | python manage.py dumpdata app.Model |
| Load data | python manage.py loaddata fixture.json |
| List routes | python manage.py show_urls |
Project setup
pip install django djangorestframework
django-admin startproject mysite .
python manage.py startapp blog
Add your app to settings.py:
INSTALLED_APPS = [
# built-in apps...
"blog",
"rest_framework",
]
Models
# blog/models.py
from django.db import models
from django.contrib.auth import get_user_model
User = get_user_model()
class Category(models.Model):
name = models.CharField(max_length=100, unique=True)
slug = models.SlugField(unique=True)
class Meta:
verbose_name_plural = "categories"
ordering = ["name"]
def __str__(self):
return self.name
class Post(models.Model):
class Status(models.TextChoices):
DRAFT = "draft", "Draft"
PUBLISHED = "published", "Published"
title = models.CharField(max_length=200)
slug = models.SlugField(unique=True)
body = models.TextField()
author = models.ForeignKey(User, on_delete=models.CASCADE, related_name="posts")
category = models.ForeignKey(Category, on_delete=models.SET_NULL, null=True, blank=True)
tags = models.ManyToManyField("Tag", blank=True)
status = models.CharField(max_length=10, choices=Status, default=Status.DRAFT)
created_at = models.DateTimeField(auto_now_add=True)
updated_at = models.DateTimeField(auto_now=True)
published_at = models.DateTimeField(null=True, blank=True)
class Meta:
ordering = ["-published_at"]
indexes = [
models.Index(fields=["-published_at"]),
models.Index(fields=["status", "-published_at"]),
]
def __str__(self):
return self.title
class Tag(models.Model):
name = models.CharField(max_length=50, unique=True)
def __str__(self):
return self.name
Field types quick reference
| Field | Use case |
|---|---|
CharField(max_length=n) |
Short text, required |
TextField() |
Long text |
SlugField() |
URL-friendly string |
IntegerField() |
Integer |
FloatField() / DecimalField(max_digits, decimal_places) |
Floats / money |
BooleanField() |
True/False |
DateField() / DateTimeField() |
Date / datetime |
auto_now_add=True |
Set once on create |
auto_now=True |
Update on every save |
EmailField() |
Validated email |
URLField() |
Validated URL |
JSONField() |
JSON (PostgreSQL recommended) |
ImageField(upload_to="images/") |
File path, requires Pillow |
ForeignKey(Model, on_delete=...) |
Many-to-one |
ManyToManyField(Model) |
Many-to-many |
OneToOneField(Model, ...) |
One-to-one |
on_delete options: CASCADE, SET_NULL, SET_DEFAULT, PROTECT, DO_NOTHING.
ORM queries
from blog.models import Post
# --- Create ---
post = Post.objects.create(title="Hello", slug="hello", author=user)
# Save after modifying
post.title = "Updated"
post.save()
# Bulk create (no signals, no save())
Post.objects.bulk_create([Post(title="A"), Post(title="B")])
# --- Read ---
Post.objects.all() # all rows
Post.objects.filter(status="published") # WHERE status = 'published'
Post.objects.exclude(status="draft") # NOT draft
Post.objects.get(slug="hello") # single row, raises DoesNotExist / MultipleObjectsReturned
Post.objects.first() # first per ordering
Post.objects.last()
Post.objects.count()
# Lookups
Post.objects.filter(title__icontains="django") # LIKE %django% (case-insensitive)
Post.objects.filter(title__startswith="How")
Post.objects.filter(created_at__year=2026)
Post.objects.filter(author__username="alice") # JOIN via FK
Post.objects.filter(tags__name="python") # JOIN via M2M
# --- Select related (avoid N+1) ---
Post.objects.select_related("author", "category") # FK / O2O → SQL JOIN
Post.objects.prefetch_related("tags") # M2M / reverse FK → separate query
# --- Ordering & slicing ---
Post.objects.order_by("-created_at")[:10] # latest 10
# --- Update ---
Post.objects.filter(status="draft").update(status="published") # bulk update
# --- Delete ---
Post.objects.filter(status="draft").delete() # bulk delete
# --- Aggregation ---
from django.db.models import Count, Avg, Max, Sum
Post.objects.aggregate(total=Count("id"), avg_len=Avg("body__length"))
# --- Annotation ---
from django.db.models import Count
Category.objects.annotate(post_count=Count("post"))
# --- Q objects (OR / NOT) ---
from django.db.models import Q
Post.objects.filter(Q(status="published") | Q(author=user))
Post.objects.filter(~Q(status="draft")) # NOT draft
# --- F objects (reference another field) ---
from django.db.models import F
Post.objects.filter(updated_at__gt=F("created_at"))
# --- Values ---
Post.objects.values("id", "title") # list of dicts
Post.objects.values_list("id", flat=True) # flat list of ids
Views
Function-based views
# blog/views.py
from django.shortcuts import render, get_object_or_404, redirect
from django.http import JsonResponse, HttpResponse
from .models import Post
def post_list(request):
posts = Post.objects.filter(status="published").select_related("author")
return render(request, "blog/post_list.html", {"posts": posts})
def post_detail(request, slug):
post = get_object_or_404(Post, slug=slug, status="published")
return render(request, "blog/post_detail.html", {"post": post})
def create_post(request):
if request.method == "POST":
# handle form
return redirect("post_list")
return render(request, "blog/create.html")
Class-based views
from django.views.generic import ListView, DetailView, CreateView, UpdateView, DeleteView
from django.urls import reverse_lazy
from django.contrib.auth.mixins import LoginRequiredMixin, PermissionRequiredMixin
class PostListView(ListView):
model = Post
template_name = "blog/post_list.html"
context_object_name = "posts"
paginate_by = 10
def get_queryset(self):
return Post.objects.filter(status="published").select_related("author")
class PostDetailView(DetailView):
model = Post
template_name = "blog/post_detail.html"
slug_field = "slug"
class PostCreateView(LoginRequiredMixin, CreateView):
model = Post
fields = ["title", "slug", "body", "category", "status"]
success_url = reverse_lazy("post_list")
def form_valid(self, form):
form.instance.author = self.request.user
return super().form_valid(form)
class PostUpdateView(LoginRequiredMixin, UpdateView):
model = Post
fields = ["title", "body", "status"]
success_url = reverse_lazy("post_list")
class PostDeleteView(LoginRequiredMixin, DeleteView):
model = Post
success_url = reverse_lazy("post_list")
URLs
# blog/urls.py
from django.urls import path
from . import views
app_name = "blog"
urlpatterns = [
path("", views.PostListView.as_view(), name="post_list"),
path("<slug:slug>/", views.PostDetailView.as_view(), name="post_detail"),
path("create/", views.PostCreateView.as_view(), name="post_create"),
path("<slug:slug>/edit/", views.PostUpdateView.as_view(), name="post_update"),
path("<slug:slug>/delete/", views.PostDeleteView.as_view(), name="post_delete"),
]
# mysite/urls.py
from django.contrib import admin
from django.urls import path, include
urlpatterns = [
path("admin/", admin.site.urls),
path("blog/", include("blog.urls", namespace="blog")),
path("api/", include("blog.api_urls")),
]
Use in templates: {% url 'blog:post_list' %} or in Python: reverse("blog:post_list").
Templates
{# blog/templates/blog/post_list.html #}
{% extends "base.html" %}
{% block content %}
<h1>Posts</h1>
{% for post in posts %}
<article>
<h2><a href="{% url 'blog:post_detail' post.slug %}">{{ post.title }}</a></h2>
<p>By {{ post.author.get_full_name }} on {{ post.created_at|date:"N j, Y" }}</p>
<p>{{ post.body|truncatewords:30 }}</p>
</article>
{% empty %}
<p>No posts yet.</p>
{% endfor %}
{# Pagination #}
{% if is_paginated %}
{% if page_obj.has_previous %}
<a href="?page={{ page_obj.previous_page_number }}">Previous</a>
{% endif %}
Page {{ page_obj.number }} of {{ page_obj.paginator.num_pages }}
{% if page_obj.has_next %}
<a href="?page={{ page_obj.next_page_number }}">Next</a>
{% endif %}
{% endif %}
{% endblock %}
Common template filters
| Filter | Example | Output |
|---|---|---|
date |
{{ dt|date:"Y-m-d" }} |
2026-07-14 |
truncatewords |
{{ text|truncatewords:20 }} |
First 20 words… |
truncatechars |
{{ text|truncatechars:100 }} |
First 100 chars… |
linebreaks |
{{ text|linebreaks }} |
<p> tags |
safe |
{{ html|safe }} |
Bypasses escaping |
default |
{{ val|default:"N/A" }} |
Fallback value |
length |
{{ list|length }} |
Count |
lower / upper |
{{ s|lower }} |
case conversion |
slugify |
{{ title|slugify }} |
url-safe-slug |
add |
{{ num|add:5 }} |
num + 5 |
Forms
# blog/forms.py
from django import forms
from .models import Post
class PostForm(forms.ModelForm):
class Meta:
model = Post
fields = ["title", "slug", "body", "category", "status"]
widgets = {
"body": forms.Textarea(attrs={"rows": 10}),
}
def clean_slug(self):
slug = self.cleaned_data["slug"]
if Post.objects.filter(slug=slug).exclude(pk=self.instance.pk).exists():
raise forms.ValidationError("A post with this slug already exists.")
return slug
# In a view:
def create_post(request):
form = PostForm(request.POST or None)
if form.is_valid():
post = form.save(commit=False)
post.author = request.user
post.save()
form.save_m2m() # save ManyToMany after commit=False
return redirect("blog:post_list")
return render(request, "blog/create.html", {"form": form})
{# template #}
<form method="post">
{% csrf_token %}
{{ form.as_p }}
<button type="submit">Save</button>
</form>
Django REST Framework
pip install djangorestframework
# blog/serializers.py
from rest_framework import serializers
from .models import Post, Category
class CategorySerializer(serializers.ModelSerializer):
class Meta:
model = Category
fields = ["id", "name", "slug"]
class PostSerializer(serializers.ModelSerializer):
author_name = serializers.CharField(source="author.get_full_name", read_only=True)
category = CategorySerializer(read_only=True)
category_id = serializers.PrimaryKeyRelatedField(
queryset=Category.objects.all(), source="category", write_only=True
)
class Meta:
model = Post
fields = ["id", "title", "slug", "body", "author_name", "category", "category_id", "status", "created_at"]
read_only_fields = ["id", "created_at"]
def validate_slug(self, value):
qs = Post.objects.filter(slug=value)
if self.instance:
qs = qs.exclude(pk=self.instance.pk)
if qs.exists():
raise serializers.ValidationError("Slug already in use.")
return value
# blog/api_views.py
from rest_framework import viewsets, permissions, filters
from rest_framework.decorators import action
from rest_framework.response import Response
from .models import Post
from .serializers import PostSerializer
class PostViewSet(viewsets.ModelViewSet):
queryset = Post.objects.select_related("author", "category").prefetch_related("tags")
serializer_class = PostSerializer
permission_classes = [permissions.IsAuthenticatedOrReadOnly]
filter_backends = [filters.SearchFilter, filters.OrderingFilter]
search_fields = ["title", "body"]
ordering_fields = ["created_at", "title"]
def get_queryset(self):
qs = super().get_queryset()
status = self.request.query_params.get("status")
if status:
qs = qs.filter(status=status)
return qs
def perform_create(self, serializer):
serializer.save(author=self.request.user)
@action(detail=False, methods=["get"])
def published(self, request):
posts = self.get_queryset().filter(status="published")
serializer = self.get_serializer(posts, many=True)
return Response(serializer.data)
# blog/api_urls.py
from rest_framework.routers import DefaultRouter
from .api_views import PostViewSet
router = DefaultRouter()
router.register("posts", PostViewSet, basename="post")
urlpatterns = router.urls
# GET /api/posts/ → list
# POST /api/posts/ → create
# GET /api/posts/{id}/ → retrieve
# PUT /api/posts/{id}/ → update
# PATCH /api/posts/{id}/ → partial update
# DELETE /api/posts/{id}/ → destroy
# GET /api/posts/published/ → custom action
Authentication
# settings.py
AUTH_USER_MODEL = "accounts.User" # custom user (recommended)
REST_FRAMEWORK = {
"DEFAULT_AUTHENTICATION_CLASSES": [
"rest_framework.authentication.SessionAuthentication",
"rest_framework_simplejwt.authentication.JWTAuthentication",
],
"DEFAULT_PERMISSION_CLASSES": [
"rest_framework.permissions.IsAuthenticatedOrReadOnly",
],
"DEFAULT_PAGINATION_CLASS": "rest_framework.pagination.PageNumberPagination",
"PAGE_SIZE": 20,
}
JWT with SimpleJWT
pip install djangorestframework-simplejwt
# urls.py
from rest_framework_simplejwt.views import TokenObtainPairView, TokenRefreshView
urlpatterns += [
path("api/token/", TokenObtainPairView.as_view()),
path("api/token/refresh/", TokenRefreshView.as_view()),
]
# Decorators for function views
from django.contrib.auth.decorators import login_required, permission_required
from rest_framework.decorators import permission_classes
from rest_framework.permissions import IsAuthenticated
@login_required
def my_view(request): ...
@permission_required("blog.add_post", raise_exception=True)
def create_post(request): ...
Admin
# blog/admin.py
from django.contrib import admin
from .models import Post, Category, Tag
@admin.register(Post)
class PostAdmin(admin.ModelAdmin):
list_display = ["title", "author", "status", "created_at"]
list_filter = ["status", "category", "created_at"]
search_fields = ["title", "body"]
prepopulated_fields = {"slug": ("title",)}
date_hierarchy = "created_at"
ordering = ["-created_at"]
raw_id_fields = ["author"]
filter_horizontal = ["tags"]
readonly_fields = ["created_at", "updated_at"]
fieldsets = [
("Content", {"fields": ["title", "slug", "body", "author"]}),
("Meta", {"fields": ["category", "tags", "status"]}),
("Timestamps", {"fields": ["created_at", "updated_at"], "classes": ["collapse"]}),
]
def get_queryset(self, request):
return super().get_queryset(request).select_related("author", "category")
admin.site.register(Category)
admin.site.register(Tag)
Signals
# blog/signals.py
from django.db.models.signals import post_save, pre_delete
from django.dispatch import receiver
from django.utils.text import slugify
from .models import Post
@receiver(post_save, sender=Post)
def auto_slugify(sender, instance, created, **kwargs):
if created and not instance.slug:
instance.slug = slugify(instance.title)
instance.save(update_fields=["slug"])
# blog/apps.py
class BlogConfig(AppConfig):
default_auto_field = "django.db.models.BigAutoField"
name = "blog"
def ready(self):
import blog.signals # noqa: F401
Common mistakes
| Mistake | Fix |
|---|---|
Post.objects.get() without try/except |
Use get_object_or_404() or catch DoesNotExist |
| N+1 queries in views | Use select_related() / prefetch_related() |
| Putting business logic in views | Move to model methods or service layer |
Not using commit=False before saving M2M |
Call form.save_m2m() after saving instance |
auto_now_add field in update_fields |
Omit it — Django ignores it anyway |
Missing {% csrf_token %} in forms |
Always include in any POST form |
Using request.user without @login_required |
Check request.user.is_authenticated first |
6 FAQ
Q: When should I use function-based views vs class-based views?
Use FBVs for simple, one-off endpoints or when the flow has many branches (complex conditionals). Use CBVs for CRUD operations — they eliminate boilerplate and mixins (LoginRequiredMixin, etc.) compose cleanly.
Q: What's the difference between null=True and blank=True?
null=True adds NULL at the database level (for non-string fields). blank=True allows empty values in Django form validation. For strings, use only blank=True; Django stores empty strings, not NULL. For non-string fields (integers, dates, FKs), use null=True, blank=True together when the field is optional.
Q: How do I avoid N+1 queries?
Use select_related("author") for ForeignKey / OneToOne (generates a single JOIN) and prefetch_related("tags") for ManyToMany / reverse FK (runs a separate optimised query). Check queries in the shell with django-debug-toolbar or connection.queries.
Q: How do I create a custom user model?
Create it before the first migration. Subclass AbstractUser (keeps existing fields) or AbstractBaseUser (full control). Set AUTH_USER_MODEL = "accounts.User" in settings.py. Reference it in code via get_user_model(), never import directly.
Q: What is makemigrations vs migrate?
makemigrations creates Python migration files by comparing your models to the current migration history. migrate actually runs those SQL statements against the database. Always run makemigrations first, commit the generated files, then run migrate in each environment.
Q: How do I handle file uploads?
Add MEDIA_URL = "/media/" and MEDIA_ROOT = BASE_DIR / "media" to settings. Use ImageField or FileField on your model. In urls.py serve with + static(settings.MEDIA_URL, document_root=settings.MEDIA_ROOT) in development. In production, serve media files via Nginx or an object store (S3/Cloudflare R2) — never via Django directly.