1. Django vs FastAPI
| Critère | Django (+ DRF) | FastAPI |
| Philosophie | Batteries included — tout fourni | Micro-framework — assemblez vos outils |
| ORM | Django ORM intégré | SQLAlchemy (à installer) |
| Admin | Interface admin auto-générée | Non intégré |
| Auth | Système auth complet (users, groups, perms) | JWT custom ou auth0 |
| Async | Partiel (Django 4.1+) | Natif (ASGI) |
| Performance | Bonne | Très haute |
| Doc API | DRF Browsable API | Swagger / ReDoc auto |
| Idéal pour | Projets complexes, backoffice, CMS | Microservices, API haute perf |
2. Démarrer un projet Django
pip install django==5.0.4 djangorestframework==3.15.1
# Créer le projet
django-admin startproject taskapi .
# Créer une application
python manage.py startapp blog
# Structure générée
# taskapi/
# ├── manage.py
# ├── taskapi/
# │ ├── settings.py
# │ ├── urls.py
# │ └── wsgi.py
# └── blog/
# ├── models.py
# ├── views.py
# ├── serializers.py ← à créer
# └── urls.py ← à créer
# taskapi/settings.py — configuration minimale
INSTALLED_APPS = [
"django.contrib.admin",
"django.contrib.auth",
"django.contrib.contenttypes",
"rest_framework", # ← DRF
"rest_framework.authtoken", # ← Token auth
"django_filters", # ← django-filter
"blog", # ← notre app
]
# Configuration DRF globale
REST_FRAMEWORK = {
"DEFAULT_AUTHENTICATION_CLASSES": [
"rest_framework.authentication.TokenAuthentication",
],
"DEFAULT_PERMISSION_CLASSES": [
"rest_framework.permissions.IsAuthenticatedOrReadOnly",
],
"DEFAULT_PAGINATION_CLASS": "rest_framework.pagination.PageNumberPagination",
"PAGE_SIZE": 20,
"DEFAULT_FILTER_BACKENDS": ["django_filters.rest_framework.DjangoFilterBackend"],
}
3. models.Model
from __future__ import annotations
from django.contrib.auth.models import User
from django.db import models
class Post(models.Model):
class Status(models.TextChoices):
DRAFT = "draft", "Brouillon"
PUBLISHED = "published", "Publié"
ARCHIVED = "archived", "Archivé"
title = models.CharField(max_length=200)
slug = models.SlugField(unique=True)
content = models.TextField()
author = models.ForeignKey(
User,
on_delete=models.CASCADE,
related_name="posts",
)
status = models.CharField(
max_length=20,
choices=Status.choices,
default=Status.DRAFT,
)
published_at = models.DateTimeField(null=True, blank=True)
created_at = models.DateTimeField(auto_now_add=True)
updated_at = models.DateTimeField(auto_now=True)
class Meta:
ordering = ["-created_at"] # ordre par défaut
indexes = [
models.Index(fields=["status", "created_at"]),
]
def __str__(self) -> str:
return self.title
class Comment(models.Model):
post = models.ForeignKey(Post, on_delete=models.CASCADE, related_name="comments")
author = models.ForeignKey(User, on_delete=models.CASCADE)
content = models.TextField()
created_at = models.DateTimeField(auto_now_add=True)
class Meta:
ordering = ["created_at"]
# Migrations
python manage.py makemigrations blog
python manage.py migrate
# Créer un superuser pour l'admin
python manage.py createsuperuser
4. Django Admin
# blog/admin.py
from __future__ import annotations
from django.contrib import admin
from django.db.models import Count
from .models import Comment, Post
class CommentInline(admin.TabularInline):
model = Comment
extra = 0
readonly_fields = ["created_at"]
@admin.register(Post)
class PostAdmin(admin.ModelAdmin):
list_display = ["title", "author", "status", "comment_count", "created_at"]
list_filter = ["status", "author"]
search_fields = ["title", "content"]
prepopulated_fields = {"slug": ("title",)} # auto-génère le slug
inlines = [CommentInline]
actions = ["publish_posts"]
def get_queryset(self, request):
return super().get_queryset(request).annotate(
comment_count=Count("comments")
)
@admin.display(description="Commentaires", ordering="comment_count")
def comment_count(self, obj):
return obj.comment_count
@admin.action(description="Publier les posts sélectionnés")
def publish_posts(self, request, queryset):
from django.utils import timezone
count = queryset.filter(status="draft").update(
status="published", published_at=timezone.now()
)
self.message_user(request, f"{count} post(s) publiés.")
5. DRF Serializer
from __future__ import annotations
from rest_framework import serializers
from django.contrib.auth.models import User
from .models import Comment, Post
class CommentSerializer(serializers.ModelSerializer):
author_name = serializers.CharField(source="author.username", read_only=True)
class Meta:
model = Comment
fields = ["id", "content", "author_name", "created_at"]
read_only_fields = ["created_at"]
class PostSerializer(serializers.ModelSerializer):
author_name = serializers.CharField(source="author.username", read_only=True)
comment_count = serializers.IntegerField(read_only=True)
comments = CommentSerializer(many=True, read_only=True)
class Meta:
model = Post
fields = [
"id", "title", "slug", "content", "status",
"author_name", "comment_count", "comments",
"published_at", "created_at",
]
read_only_fields = ["slug", "created_at", "published_at"]
# Validator personnalisé
def validate_title(self, value: str) -> str:
if len(value) < 5:
raise serializers.ValidationError("Le titre doit faire au moins 5 caractères")
return value.strip()
# Validation cross-fields (équivalent model_validator)
def validate(self, attrs: dict) -> dict:
if attrs.get("status") == "published" and not attrs.get("content"):
raise serializers.ValidationError("Un post publié doit avoir un contenu")
return attrs
def create(self, validated_data: dict) -> Post:
# Injecter l'auteur depuis la requête
validated_data["author"] = self.context["request"].user
from django.utils.text import slugify
validated_data["slug"] = slugify(validated_data["title"])
return super().create(validated_data)
6. ViewSet & Router
from __future__ import annotations
from django.db.models import Count
from rest_framework import status, viewsets
from rest_framework.decorators import action
from rest_framework.permissions import IsAuthenticatedOrReadOnly
from rest_framework.request import Request
from rest_framework.response import Response
from .models import Comment, Post
from .serializers import CommentSerializer, PostSerializer
class PostViewSet(viewsets.ModelViewSet):
"""CRUD complet pour les posts du blog."""
serializer_class = PostSerializer
permission_classes = [IsAuthenticatedOrReadOnly]
def get_queryset(self):
# Optimisation : annotate + select_related en une requête
return (
Post.objects
.select_related("author")
.prefetch_related("comments")
.annotate(comment_count=Count("comments"))
.filter(status="published") # n'expose que les posts publiés
)
# ── Action personnalisée : POST /posts/{id}/publish/
@action(detail=True, methods=["post"])
def publish(self, request: Request, pk: int = None) -> Response:
post = self.get_object()
if post.status == "published":
return Response({"detail": "Déjà publié"}, status=400)
post.status = "published"
from django.utils import timezone
post.published_at = timezone.now()
post.save()
return Response(PostSerializer(post).data)
# ── Action liste : GET /posts/drafts/
@action(detail=False, methods=["get"])
def drafts(self, request: Request) -> Response:
drafts = Post.objects.filter(author=request.user, status="draft")
serializer = self.get_serializer(drafts, many=True)
return Response(serializer.data)
class CommentViewSet(viewsets.ModelViewSet):
serializer_class = CommentSerializer
permission_classes = [IsAuthenticatedOrReadOnly]
def get_queryset(self):
return Comment.objects.filter(post_id=self.kwargs["post_pk"])
def perform_create(self, serializer: CommentSerializer) -> None:
serializer.save(
author=self.request.user,
post_id=self.kwargs["post_pk"],
)
# blog/urls.py — Router DRF
from __future__ import annotations
from rest_framework.routers import DefaultRouter
from rest_framework_nested import routers as nested_routers
from . import views
router = DefaultRouter()
router.register(r"posts", views.PostViewSet, basename="post")
# Routes générées automatiquement :
# GET /posts/ → list
# POST /posts/ → create
# GET /posts/{id}/ → retrieve
# PUT /posts/{id}/ → update
# PATCH /posts/{id}/ → partial_update
# DELETE /posts/{id}/ → destroy
# POST /posts/{id}/publish/ → publish (action)
# GET /posts/drafts/ → drafts (action)
urlpatterns = router.urls
7. TokenAuthentication
from __future__ import annotations
from django.contrib.auth.models import User
from rest_framework import status
from rest_framework.authtoken.models import Token
from rest_framework.decorators import api_view, permission_classes
from rest_framework.permissions import AllowAny
from rest_framework.request import Request
from rest_framework.response import Response
@api_view(["POST"])
@permission_classes([AllowAny])
def register(request: Request) -> Response:
username = request.data.get("username")
password = request.data.get("password")
email = request.data.get("email", "")
if not username or not password:
return Response({"error": "username et password requis"}, status=400)
if User.objects.filter(username=username).exists():
return Response({"error": "Username déjà pris"}, status=409)
user = User.objects.create_user(username=username, password=password, email=email)
token, _ = Token.objects.get_or_create(user=user)
return Response({"token": token.key, "user_id": user.id}, status=201)
@api_view(["POST"])
@permission_classes([AllowAny])
def login_view(request: Request) -> Response:
from django.contrib.auth import authenticate
user = authenticate(
username=request.data.get("username"),
password=request.data.get("password"),
)
if user is None:
return Response({"error": "Identifiants incorrects"}, status=401)
token, _ = Token.objects.get_or_create(user=user)
return Response({"token": token.key})
# ── Usage côté client
# curl -X POST /api/login -d '{"username":"alice","password":"xxx"}'
# → {"token": "9944b09199c62bcf9418ad846dd0e4bbdfc6ee4b"}
# curl /api/posts -H "Authorization: Token 9944b09..."
8. Filtres & Pagination DRF
from __future__ import annotations
import django_filters
from rest_framework import filters, pagination
from .models import Post
# ── Filtre personnalisé
class PostFilter(django_filters.FilterSet):
status = django_filters.ChoiceFilter(choices=Post.Status.choices)
author = django_filters.CharFilter(field_name="author__username", lookup_expr="iexact")
created_after = django_filters.DateFilter(field_name="created_at", lookup_expr="gte")
title_contains = django_filters.CharFilter(field_name="title", lookup_expr="icontains")
class Meta:
model = Post
fields = ["status", "author", "created_after", "title_contains"]
# ── Pagination personnalisée
class StandardPagination(pagination.PageNumberPagination):
page_size = 20
page_size_query_param = "limit"
max_page_size = 100
page_query_param = "page"
def get_paginated_response(self, data):
return Response({
"count": self.page.paginator.count,
"pages": self.page.paginator.num_pages,
"next": self.get_next_link(),
"previous": self.get_previous_link(),
"results": data,
})
# ── Dans le ViewSet
class PostViewSet(viewsets.ModelViewSet):
filterset_class = PostFilter
pagination_class = StandardPagination
filter_backends = [
django_filters.rest_framework.DjangoFilterBackend,
filters.SearchFilter,
filters.OrderingFilter,
]
search_fields = ["title", "content"]
ordering_fields = ["created_at", "title"]
ordering = ["-created_at"]
9. ORM Django avancé (annotate, Q)
from __future__ import annotations
from django.db.models import Avg, Count, ExpressionWrapper, F, FloatField, Q, Sum
from django.db.models.functions import TruncMonth
from django.utils import timezone
from .models import Post
# ── annotate : ajouter des champs calculés
posts_with_stats = Post.objects.annotate(
comment_count=Count("comments"),
# Sous-requête / expression
days_since_published=ExpressionWrapper(
timezone.now() - F("published_at"),
output_field=FloatField(),
),
)
# ── Q objects : requêtes complexes
from django.db.models import Q
# Posts publiés OU brouillons de l'utilisateur courant
my_posts = Post.objects.filter(
Q(status="published") | Q(author=request.user, status="draft")
)
# Posts avec titre contenant "python" ET au moins 1 commentaire
popular_python = Post.objects.filter(
Q(title__icontains="python") &
Q(comment_count__gte=1)
).annotate(comment_count=Count("comments"))
# ── Agrégation globale
stats = Post.objects.aggregate(
total=Count("id"),
published=Count("id", filter=Q(status="published")),
avg_comments=Avg("comments"),
)
# ── Grouper par mois
monthly = (
Post.objects
.filter(status="published")
.annotate(month=TruncMonth("published_at"))
.values("month")
.annotate(count=Count("id"))
.order_by("month")
)