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.

Lance-toi en Python Django REST Framework : ton premier pas concret aujourd'hui

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

output
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

AspectRESTGraphQLRPC (gRPC)
FormatJSON/XMLJSONProtobuf
TransportHTTPHTTPHTTP/2
SchemaOpenAPISDL (typed).proto
Versioningv1/v2deprecated fieldsbackward compat
Over-fetchOuiNonNon
Cache HTTPFacileDifficileNon
ToolingExcellentBonTech-specific
Mobile/IoTOKExcellentExcellent

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

output
# 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

output
{
  "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

output
# 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

output
# 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

output
# 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

output
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

output
# 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

output
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

output
# 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

output
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

output
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

output
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

output
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

output
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 Coding

FAQ

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.

📬 Tu veux recevoir ce type de guide chaque semaine ? Abonne-toi gratuitement — code réel, zéro blabla.