Lance-toi en Python Django REST Framework : ton premier pas concret aujourd'hui
Python Django REST Framework : l'essentiel en un article — vrai code, schémas et étapes concrètes, extraits d'un cours de 24 leçons.
La meilleure façon d'apprendre Python Django REST Framework, c'est de faire. Cet article te met le pied à l'étrier avec des extraits pratiques tirés d'un cours de 24 leçons — de quoi obtenir un premier résultat dès aujourd'hui.
tl;dr
- Setup DRF
- Serializers
- Views
- Auth et permissions
- Filtres et pagination
~$ cat ./parcours.md # Python Django REST Framework — 8 chapitres
01
Setup DRF
→ REST vs RPC vs GraphQL→ Installation et premier endpoint+ 1 autres leçons
02
Serializers
→ Serializer et ModelSerializer→ Validation custom+ 1 autres leçons
03
Views
→ APIView et generics→ ViewSets et Routers+ 1 autres leçons
04
Auth et permissions
→ Token, Session, JWT→ Permissions builtin et custom+ 1 autres leçons
05
Filtres et pagination
→ Filters et search→ Pagination+ 1 autres leçons
06
Documentation OpenAPI
→ drf-spectacular→ Swagger UI et Redoc+ 1 autres leçons
07
Tests et performance
→ APITestCase et factories→ Optimisation N+1+ 1 autres leçons
08
Projet final API SaaS
→ Architecture multi-tenant→ Endpoints et tests+ 1 autres leçons
🏁
Projet final
→ Tu repars avec un projet concret et démontrable
REST vs RPC vs GraphQL
REST : ressources et verbes
GET /api/articles/ # lister GET /api/articles/42/ # detail POST /api/articles/ # creer PUT /api/articles/42/ # remplacer PATCH /api/articles/42/ # partial update DELETE /api/articles/42/ # supprimer # Verbes HTTP + URIs = CRUD universel
Comparaison
| Aspect | REST | GraphQL | RPC (gRPC) |
|---|---|---|---|
| Format | JSON/XML | JSON | Protobuf |
| Transport | HTTP | HTTP | HTTP/2 |
| Schema | OpenAPI | SDL (typed) | .proto |
| Versioning | v1/v2 | deprecated fields | backward compat |
| Over-fetch | Oui | Non | Non |
| Cache HTTP | Facile | Difficile | Non |
| Tooling | Excellent | Bon | Tech-specific |
| Mobile/IoT | OK | Excellent | Excellent |
Quand choisir quoi
NOTEREST — APIs publiques, CRUD simple, web/mobile classique, cacheable. Defaut pour 90% des cas.
NOTEGraphQL — Mobile complexe avec ecran agregatant 5+ ressources, frontends multiples avec besoins differents.
NOTEgRPC — Microservices internes, latence critique, streaming bidirectionnel, langages multiples.
Pourquoi DRF ?
Alternatives a DRF
Anatomie d'une bonne API REST
# Bonnes pratiques GET /api/v1/users/ # liste GET /api/v1/users/42/ # detail GET /api/v1/users/42/orders/ # sous-ressource POST /api/v1/users/ # 201 Created + Location PATCH /api/v1/users/42/ # partial DELETE /api/v1/users/42/ # 204 No Content # Filters / pagination GET /api/v1/users/?role=admin&page=2&page_size=20 # Codes status 200 OK, 201 Created, 204 No Content 400 Bad Request, 401 Unauthorized, 403 Forbidden 404 Not Found, 409 Conflict, 422 Unprocessable 500 Server Error, 503 Unavailable
Format de reponse standard
{
"data": [...],
"meta": {
"count": 150,
"page": 2,
"page_size": 20
},
"links": {
"next": "/api/v1/users/?page=3",
"prev": "/api/v1/users/?page=1"
}
}
# Erreurs
{
"error": {
"code": "VALIDATION_ERROR",
"message": "Email is required",
"details": {"email": ["This field is required"]}
}
}Resume
NOTEA retenir
- REST = ressources + verbes HTTP standardises
- GraphQL pour mobile complexe, REST pour le reste
- DRF = standard Django depuis 2014
- v1/v2 dans URL, format JSON, codes HTTP corrects
Architecture multi-tenant
Cahier des charges
NOTEProjet — API SaaS de gestion de projets/taches multi-tenants avec plans tarifaires (Free/Pro/Enterprise), facturation Stripe, webhooks et OpenAPI public.
Modeles
# workspaces/models.py class Workspace(models.Model): class Plan(models.TextChoices): FREE = "free", "Free" PRO = "pro", "Pro" ENTERPRISE = "enterprise", "Enterprise" name = models.CharField(max_length=100) slug = models.SlugField(unique=True) plan = models.CharField(max_length=20, choices=Plan.choices, default=Plan.FREE) stripe_customer_id = models.CharField(max_length=100, blank=True) created_at = models.DateTimeField(auto_now_add=True) class Membership(models.Model): class Role(models.TextChoices): OWNER = "owner" ADMIN = "admin" MEMBER = "member" GUEST = "guest" workspace = models.ForeignKey(Workspace, on_delete=models.CASCADE, related_name="memberships") user = models.ForeignKey("accounts.User", on_delete=models.CASCADE) role = models.CharField(max_length=20, choices=Role.choices, default=Role.MEMBER) invited_at = models.DateTimeField(auto_now_add=True) class Meta: unique_together = ["workspace", "user"] # projects/models.py class Project(models.Model): workspace = models.ForeignKey(Workspace, on_delete=models.CASCADE, related_name="projects") name = models.CharField(max_length=200) description = models.TextField(blank=True) created_by = models.ForeignKey("accounts.User", on_delete=models.PROTECT) created_at = models.DateTimeField(auto_now_add=True) class Meta: indexes = [models.Index(fields=["workspace", "-created_at"])] class Task(models.Model): class Status(models.TextChoices): TODO = "todo" IN_PROGRESS = "in_progress" DONE = "done" project = models.ForeignKey(Project, on_delete=models.CASCADE, related_name="tasks") title = models.CharField(max_length=200) description = models.TextField(blank=True) status = models.CharField(max_length=20, choices=Status.choices, default=Status.TODO) assigned_to = models.ForeignKey("accounts.User", null=True, blank=True, on_delete=models.SET_NULL) due_date = models.DateField(null=True, blank=True) created_at = models.DateTimeField(auto_now_add=True) updated_at = models.DateTimeField(auto_now=True)
Tenant via subdomain
# workspaces/middleware.py class WorkspaceMiddleware: def __init__(self, get_response): self.get_response = get_response def __call__(self, request): host = request.get_host().split(":")[0] subdomain = host.split(".")[0] try: request.workspace = Workspace.objects.get(slug=subdomain) except Workspace.DoesNotExist: request.workspace = None return self.get_response(request) # settings.py MIDDLEWARE += ["workspaces.middleware.WorkspaceMiddleware"] # URLs : acme.api.example.com -> workspace "acme"
Permission multi-tenant
# workspaces/permissions.py from rest_framework.permissions import BasePermission class IsWorkspaceMember(BasePermission): def has_permission(self, request, view): if not request.user.is_authenticated: return False if not request.workspace: return False return Membership.objects.filter( workspace=request.workspace, user=request.user ).exists() class IsWorkspaceAdmin(BasePermission): def has_permission(self, request, view): return Membership.objects.filter( workspace=request.workspace, user=request.user, role__in=["owner", "admin"] ).exists()
Mixin pour queryset auto-filtre
class WorkspaceQuerysetMixin: """Filtre auto par workspace courant.""" workspace_field = "workspace" def get_queryset(self): qs = super().get_queryset() if self.request.workspace: return qs.filter(**{self.workspace_field: self.request.workspace}) return qs.none() def perform_create(self, serializer): serializer.save(workspace=self.request.workspace, created_by=self.request.user) class ProjectViewSet(WorkspaceQuerysetMixin, viewsets.ModelViewSet): queryset = Project.objects.all() serializer_class = ProjectSerializer permission_classes = [IsAuthenticated, IsWorkspaceMember] def get_queryset(self): return super().get_queryset().select_related("created_by") class TaskViewSet(WorkspaceQuerysetMixin, viewsets.ModelViewSet): queryset = Task.objects.all() serializer_class = TaskSerializer permission_classes = [IsAuthenticated, IsWorkspaceMember] workspace_field = "project__workspace" def get_queryset(self): return super().get_queryset().select_related("project", "assigned_to")
Quotas par plan
# workspaces/quotas.py PLAN_LIMITS = { "free": {"max_projects": 3, "max_members": 3, "api_rate": "100/day"}, "pro": {"max_projects": None, "max_members": 20, "api_rate": "10000/day"}, "enterprise": {"max_projects": None, "max_members": None, "api_rate": None}, } def check_project_quota(workspace): limit = PLAN_LIMITS[workspace.plan]["max_projects"] if limit is None: return current = workspace.projects.count() if current >= limit: raise serializers.ValidationError( f"Limite atteinte : {limit} projects pour le plan {workspace.plan}. " f"Upgrade vers Pro." ) # Dans le serializer class ProjectSerializer(serializers.ModelSerializer): def create(self, validated_data): check_project_quota(validated_data["workspace"]) return super().create(validated_data)
Throttle par plan
class PlanRateThrottle(SimpleRateThrottle): scope = "plan" def allow_request(self, request, view): if not request.workspace: return True rate = PLAN_LIMITS[request.workspace.plan]["api_rate"] if rate is None: return True # enterprise self.num_requests, self.duration = self.parse_rate(rate) self.request = request return super().allow_request(request, view) def get_cache_key(self, request, view): return f"plan_{request.workspace.id}"
Caching reponses
Configurer le cache
# settings.py - Redis backend CACHES = { "default": { "BACKEND": "django.core.cache.backends.redis.RedisCache", "LOCATION": env("REDIS_URL"), "TIMEOUT": 300, # 5 min defaut "OPTIONS": { "db": 1, "max_connections": 50, } } } # Test rapide avec local memory (dev) CACHES = { "default": {"BACKEND": "django.core.cache.backends.locmem.LocMemCache"} }
Cache de vue complete
from django.views.decorators.cache import cache_page from django.utils.decorators import method_decorator class PopularBooksView(generics.ListAPIView): queryset = Book.objects.filter(popular=True) serializer_class = BookSerializer @method_decorator(cache_page(60 * 15)) # 15 min def dispatch(self, *args, **kwargs): return super().dispatch(*args, **kwargs) # OU avec vary_on_headers from django.views.decorators.vary import vary_on_headers @method_decorator(cache_page(600), name="dispatch") @method_decorator(vary_on_headers("Authorization"), name="dispatch") class MyView(generics.ListAPIView): # Cache different par token ...
Cache low-level
from django.core.cache import cache class StatsView(APIView): def get(self, request): cache_key = f"stats_{request.user.id}" data = cache.get(cache_key) if data is None: # Calcul couteux data = compute_expensive_stats(request.user) cache.set(cache_key, data, timeout=3600) return Response(data) # get_or_set en 1 ligne data = cache.get_or_set(f"stats_{user.id}", lambda: compute_stats(user), 3600) # Multi-cache cache.set_many({"a": 1, "b": 2}, timeout=60) values = cache.get_many(["a", "b"]) # Invalidation cache.delete("stats_42") cache.delete_many(["stats_42", "stats_43"]) cache.clear() # tout effacer
Cache invalidation via signals
from django.db.models.signals import post_save, post_delete from django.dispatch import receiver @receiver([post_save, post_delete], sender=Book) def invalidate_books_cache(sender, instance, **kwargs): cache.delete("popular_books") cache.delete(f"book_{instance.id}") cache.delete(f"books_by_author_{instance.author_id}")
HTTP cache : ETag
from rest_framework.response import Response import hashlib class BookDetailView(generics.RetrieveAPIView): queryset = Book.objects.all() serializer_class = BookSerializer def retrieve(self, request, *args, **kwargs): book = self.get_object() etag = hashlib.md5(str(book.updated_at).encode()).hexdigest() if request.headers.get("If-None-Match") == etag: return Response(status=304) # Not Modified response = Response(BookSerializer(book).data) response["ETag"] = etag response["Cache-Control"] = "public, max-age=300" return response # Client renvoie automatiquement If-None-Match: "abc..." # Serveur peut repondre 304 sans body -> tres rapide
Cache-Control par endpoint
from django.views.decorators.cache import cache_control from django.utils.decorators import method_decorator class PublicCatalogView(generics.ListAPIView): queryset = Product.objects.filter(public=True) serializer_class = ProductSerializer @method_decorator(cache_control(public=True, max_age=3600)) def dispatch(self, *args, **kwargs): return super().dispatch(*args, **kwargs) # Header : Cache-Control: public, max-age=3600 # CDN et browser le cacheront class UserDataView(APIView): @method_decorator(cache_control(private=True, max_age=60)) def dispatch(self, *args, **kwargs): return super().dispatch(*args, **kwargs) # private = pas de cache CDN, mais browser oui
va-plus-loin
Cet article couvre les extraits les plus utiles — le cours complet Python Django REST Framework (8 chapitres, 24 leçons, exercices corrigés et projet final) t'emmène jusqu'au bout.
./acceder-au-cours-complet cours gratuit : Vibe CodingFAQ
Combien de temps pour apprendre Python Django REST Framework ?
Avec une progression structurée (8 chapitres, 24 leçons courtes et pratiques), on atteint un niveau opérationnel en quelques semaines à raison de 30 à 60 minutes par jour. L'important est de pratiquer chaque notion immédiatement.
Faut-il des prérequis ?
Des bases en informatique suffisent. Si tu sais utiliser un terminal et lire du code simple, tu es prêt.
Par où commencer concrètement ?
Reproduis les commandes de cet article, puis suis le cours complet Python Django REST Framework : il enchaîne les 24 leçons dans l'ordre, avec exercices et projet final.
./a-lire-aussi
→ Lance-toi en Portfolio IA SEO Vercel : ton premier pas concret aujourd'hui→ IA Stripe GitHub SaaS en pratique : le code et les commandes qui comptent vraiment→ Python Requests APIs expliqué simplement (avec schémas et vrai code)📬 Tu veux recevoir ce type de guide chaque semaine ? Abonne-toi gratuitement — code réel, zéro blabla.