ITSkillsCenter
Blog

Django REST Framework en pratique : guide API moderne

13 دقائق للقراءة

Lecture : 13 minutes · Niveau : intermédiaire · Mise à jour : avril 2026

Django REST Framework (DRF) est le standard pour bâtir des API REST en Django. Mature, riche, productif, il couvre la quasi-totalité des besoins API modernes : sérialisation, validation, authentification, permissions, pagination, throttling, filtres, documentation automatique. Ce guide rassemble les patterns vraiment utiles pour livrer des API robustes en production.

DRF a été créé en 2014 et a évolué constamment depuis. Sa stabilité d’API est l’une des raisons de son succès : un projet écrit avec DRF il y a cinq ans fonctionne encore aujourd’hui avec des ajustements minimes. Pour une PME qui veut investir dans une API durable sans se préoccuper de migrations majeures tous les six mois, c’est un atout important.

L’approche DRF se distingue par sa philosophie déclarative : on décrit la forme des données via des serializers, le comportement via des viewsets, les règles d’accès via des permissions. Le framework assemble le tout. Cette séparation des préoccupations rend le code testable, lisible, et facilement modifiable. Pour des équipes qui livrent des APIs internes ou publiques régulièrement, DRF accélère drastiquement la vélocité.

L’apprentissage initial demande de comprendre quelques concepts spécifiques (serializers, viewsets, routers, permissions) qui peuvent dérouter venant d’autres frameworks comme Express ou Flask. Une fois ces concepts intégrés, la productivité monte rapidement.

Voir aussi → Django pour PME : guide backend Python.


Sommaire

  1. Setup DRF dans un projet Django
  2. Serializers : sérialiser et valider
  3. Views et ViewSets
  4. Routers et URLs
  5. Filtres, recherche, ordering
  6. Pagination
  7. Permissions et authentification
  8. JWT avec SimpleJWT
  9. Throttling et rate limiting
  10. Documentation automatique avec drf-spectacular
  11. Tests d’API
  12. FAQ

1. Setup DRF dans un projet Django

uv add djangorestframework
# settings.py
INSTALLED_APPS = [
    # ...
    'rest_framework',
]

REST_FRAMEWORK = {
    'DEFAULT_AUTHENTICATION_CLASSES': [
        'rest_framework.authentication.SessionAuthentication',
        'rest_framework.authentication.TokenAuthentication',
    ],
    'DEFAULT_PERMISSION_CLASSES': [
        'rest_framework.permissions.IsAuthenticated',
    ],
    'DEFAULT_PAGINATION_CLASS': 'rest_framework.pagination.PageNumberPagination',
    'PAGE_SIZE': 20,
    'DEFAULT_FILTER_BACKENDS': [
        'django_filters.rest_framework.DjangoFilterBackend',
        'rest_framework.filters.SearchFilter',
        'rest_framework.filters.OrderingFilter',
    ],
    'DEFAULT_THROTTLE_CLASSES': [
        'rest_framework.throttling.AnonRateThrottle',
        'rest_framework.throttling.UserRateThrottle',
    ],
    'DEFAULT_THROTTLE_RATES': {
        'anon': '100/hour',
        'user': '1000/hour',
    },
}

Cette configuration centralisée définit le comportement par défaut de tous les endpoints DRF. Override possible par viewset si besoin spécifique.


2. Serializers : sérialiser et valider

Les serializers transforment les modèles Django en JSON et vice-versa, avec validation.

# clients/serializers.py
from rest_framework import serializers
from .models import Client, Order

class OrderSerializer(serializers.ModelSerializer):
    class Meta:
        model = Order
        fields = ['id', 'reference', 'total', 'status', 'created_at']

class ClientSerializer(serializers.ModelSerializer):
    orders_count = serializers.IntegerField(read_only=True)
    full_address = serializers.SerializerMethodField()

    class Meta:
        model = Client
        fields = [
            'id', 'nom', 'email', 'telephone', 'secteur', 'actif',
            'orders_count', 'full_address', 'created_at',
        ]
        read_only_fields = ['id', 'created_at']
        extra_kwargs = {
            'email': {'required': True},
        }

    def get_full_address(self, obj):
        if not obj.address:
            return None
        return f"{obj.address.rue}, {obj.address.ville}"

    def validate_email(self, value):
        if 'example.test' in value:
            raise serializers.ValidationError("Email de test non accepté.")
        return value

    def validate(self, data):
        # Validation cross-field si besoin
        if data.get('actif') and not data.get('email'):
            raise serializers.ValidationError("Email obligatoire pour client actif.")
        return data

Serializers imbriqués

Pour exposer une relation imbriquée :

class ClientWithOrdersSerializer(serializers.ModelSerializer):
    orders = OrderSerializer(many=True, read_only=True)

    class Meta:
        model = Client
        fields = ['id', 'nom', 'orders']

Pour la création avec relations imbriquées : surcharger create() et update(). C’est verbeux mais permet un contrôle précis.

Serializers différents selon action

class ClientListSerializer(serializers.ModelSerializer):
    class Meta:
        model = Client
        fields = ['id', 'nom', 'email']  # léger pour la liste

class ClientDetailSerializer(serializers.ModelSerializer):
    orders = OrderSerializer(many=True, read_only=True)
    class Meta:
        model = Client
        fields = '__all__'

# Dans le ViewSet
def get_serializer_class(self):
    if self.action == 'list':
        return ClientListSerializer
    return ClientDetailSerializer

Optimisation : la liste retourne moins de champs, plus rapide.


3. Views et ViewSets

DRF propose plusieurs niveaux d’abstraction.

@api_view (function-based, simple)

from rest_framework.decorators import api_view
from rest_framework.response import Response

@api_view(['GET', 'POST'])
def client_list(request):
    if request.method == 'GET':
        clients = Client.objects.filter(actif=True)
        serializer = ClientSerializer(clients, many=True)
        return Response(serializer.data)
    elif request.method == 'POST':
        serializer = ClientSerializer(data=request.data)
        if serializer.is_valid():
            serializer.save()
            return Response(serializer.data, status=201)
        return Response(serializer.errors, status=400)

Class-based generic views

from rest_framework import generics

class ClientList(generics.ListCreateAPIView):
    queryset = Client.objects.filter(actif=True)
    serializer_class = ClientSerializer

class ClientDetail(generics.RetrieveUpdateDestroyAPIView):
    queryset = Client.objects.all()
    serializer_class = ClientSerializer

ViewSets (recommandé)

from rest_framework import viewsets

class ClientViewSet(viewsets.ModelViewSet):
    queryset = Client.objects.all()
    serializer_class = ClientSerializer

    def get_queryset(self):
        qs = super().get_queryset()
        if self.action == 'list':
            qs = qs.filter(actif=True).select_related('country').prefetch_related('orders')
        return qs

    def perform_create(self, serializer):
        serializer.save(created_by=self.request.user)

    @action(detail=True, methods=['post'])
    def archiver(self, request, pk=None):
        client = self.get_object()
        client.actif = False
        client.save()
        return Response({'status': 'archivé'})

@action(detail=True) ajoute des actions custom à un ViewSet (par exemple POST /clients/1/archiver/).

ViewSets + Routers = boilerplate minimal pour des CRUD complets.


4. Routers et URLs

# api/urls.py
from rest_framework.routers import DefaultRouter
from clients.views import ClientViewSet
from orders.views import OrderViewSet

router = DefaultRouter()
router.register(r'clients', ClientViewSet)
router.register(r'orders', OrderViewSet)

urlpatterns = router.urls
# monsite/urls.py
urlpatterns = [
    path('api/', include('api.urls')),
]

Le router génère automatiquement les URLs :
GET /api/clients/ (liste)
POST /api/clients/ (création)
GET /api/clients/1/ (détail)
PUT /api/clients/1/ (update complet)
PATCH /api/clients/1/ (update partiel)
DELETE /api/clients/1/ (suppression)
POST /api/clients/1/archiver/ (action custom)


5. Filtres, recherche, ordering

uv add django-filter
class ClientViewSet(viewsets.ModelViewSet):
    queryset = Client.objects.all()
    serializer_class = ClientSerializer
    filterset_fields = ['secteur', 'actif']
    search_fields = ['nom', 'email']
    ordering_fields = ['nom', 'created_at']
    ordering = ['-created_at']

Usage :
GET /api/clients/?secteur=agro&actif=true
GET /api/clients/?search=acme
GET /api/clients/?ordering=-created_at

FilterSet custom

import django_filters

class ClientFilter(django_filters.FilterSet):
    created_after = django_filters.DateFilter(field_name='created_at', lookup_expr='gte')
    created_before = django_filters.DateFilter(field_name='created_at', lookup_expr='lte')
    nom = django_filters.CharFilter(lookup_expr='icontains')

    class Meta:
        model = Client
        fields = ['secteur', 'actif', 'nom', 'created_after', 'created_before']

class ClientViewSet(viewsets.ModelViewSet):
    filterset_class = ClientFilter

Permet des filtres plus sophistiqués (intervalles de dates, lookup spécifiques).


6. Pagination

Trois styles fournis par DRF.

PageNumberPagination (défaut)

REST_FRAMEWORK = {
    'DEFAULT_PAGINATION_CLASS': 'rest_framework.pagination.PageNumberPagination',
    'PAGE_SIZE': 20,
}

Réponse :

{
  "count": 1234,
  "next": "https://api.example.com/clients/?page=3",
  "previous": "https://api.example.com/clients/?page=1",
  "results": [...]
}

LimitOffsetPagination

?limit=20&offset=40 — flexible mais inefficace sur gros datasets.

CursorPagination

class CursorClientPagination(CursorPagination):
    page_size = 20
    ordering = '-created_at'

Performant sur millions de lignes (utilise un cursor sur un champ indexé). Pas de page jump, mais next/previous fluides.

Pour des feeds infinis (timeline, listes longues) : CursorPagination. Pour de la pagination classique avec choix de page : PageNumber.


7. Permissions et authentification

Permissions intégrées

from rest_framework.permissions import (
    AllowAny, IsAuthenticated, IsAdminUser,
    IsAuthenticatedOrReadOnly, DjangoModelPermissions,
)

class ClientViewSet(viewsets.ModelViewSet):
    permission_classes = [IsAuthenticated, DjangoModelPermissions]

Custom permission

class IsOwnerOrReadOnly(BasePermission):
    def has_object_permission(self, request, view, obj):
        if request.method in SAFE_METHODS:
            return True
        return obj.created_by == request.user
class ClientViewSet(viewsets.ModelViewSet):
    permission_classes = [IsAuthenticated, IsOwnerOrReadOnly]

Permissions par action

def get_permissions(self):
    if self.action in ['list', 'retrieve']:
        return [IsAuthenticated()]
    return [IsAdminUser()]

8. JWT avec SimpleJWT

uv add djangorestframework-simplejwt
# settings.py
REST_FRAMEWORK = {
    'DEFAULT_AUTHENTICATION_CLASSES': [
        'rest_framework_simplejwt.authentication.JWTAuthentication',
    ],
}

SIMPLE_JWT = {
    'ACCESS_TOKEN_LIFETIME': timedelta(minutes=15),
    'REFRESH_TOKEN_LIFETIME': timedelta(days=7),
    'ROTATE_REFRESH_TOKENS': True,
    'BLACKLIST_AFTER_ROTATION': True,
}
# urls.py
from rest_framework_simplejwt.views import TokenObtainPairView, TokenRefreshView

urlpatterns = [
    path('api/token/', TokenObtainPairView.as_view()),
    path('api/token/refresh/', TokenRefreshView.as_view()),
]

Workflow client :
1. POST /api/token/ avec username/password → reçoit access + refresh
2. Utilise access dans header Authorization: Bearer ...
3. Quand access expire (401) → POST /api/token/refresh/ avec refresh → nouvel access
4. Logout → blacklist le refresh

Pour SPA et apps mobiles : standard 2026.


9. Throttling et rate limiting

Protection contre brute-force et abus.

REST_FRAMEWORK = {
    'DEFAULT_THROTTLE_CLASSES': [
        'rest_framework.throttling.AnonRateThrottle',
        'rest_framework.throttling.UserRateThrottle',
    ],
    'DEFAULT_THROTTLE_RATES': {
        'anon': '100/hour',
        'user': '1000/hour',
        'login': '5/minute',
    },
}

Throttle ciblé

class LoginThrottle(AnonRateThrottle):
    scope = 'login'

class LoginView(APIView):
    throttle_classes = [LoginThrottle]
    # ...

Indispensable sur endpoints sensibles : login, password reset, OTP. Sans throttling, un attaquant peut tenter des milliers de combinaisons login/password à la seconde sur une API. Quelques lignes de configuration neutralisent cette classe d’attaque.


10. Documentation automatique avec drf-spectacular

uv add drf-spectacular
INSTALLED_APPS += ['drf_spectacular']

REST_FRAMEWORK['DEFAULT_SCHEMA_CLASS'] = 'drf_spectacular.openapi.AutoSchema'

SPECTACULAR_SETTINGS = {
    'TITLE': 'Mon API',
    'DESCRIPTION': 'API de la PME Acme',
    'VERSION': '1.0.0',
}
from drf_spectacular.views import SpectacularAPIView, SpectacularSwaggerView

urlpatterns += [
    path('api/schema/', SpectacularAPIView.as_view()),
    path('api/docs/', SpectacularSwaggerView.as_view()),
]

Génère un schéma OpenAPI 3 + UI Swagger accessible sur /api/docs/. Documentation auto-générée depuis serializers, views, types.

Pour des cas complexes : @extend_schema décorateur sur les vues pour enrichir la doc avec exemples, descriptions custom, types de réponse spécifiques par status code. La documentation devient un livrable maintenu en parallèle du code, plutôt qu’un document Word qui se désynchronise rapidement.


11. Tests d’API

# tests/test_clients_api.py
from rest_framework.test import APITestCase
from rest_framework import status
from django.urls import reverse
from clients.models import Client
from users.models import User

class ClientAPITest(APITestCase):
    def setUp(self):
        self.user = User.objects.create_user(username='test', password='secret')
        self.client.force_authenticate(user=self.user)

    def test_list_clients(self):
        Client.objects.create(nom='Acme', email='a@test.com')
        url = reverse('client-list')
        response = self.client.get(url)
        self.assertEqual(response.status_code, status.HTTP_200_OK)
        self.assertEqual(len(response.data['results']), 1)

    def test_create_client(self):
        url = reverse('client-list')
        data = {'nom': 'Beta', 'email': 'b@test.com'}
        response = self.client.post(url, data, format='json')
        self.assertEqual(response.status_code, status.HTTP_201_CREATED)

    def test_invalid_email(self):
        url = reverse('client-list')
        data = {'nom': 'Test', 'email': 'pas-un-email'}
        response = self.client.post(url, data, format='json')
        self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST)

APITestCase fournit un client REST de test. Très lisible.

Avec pytest + pytest-django : encore plus expressif via fixtures.

Stratégie de tests d’API

Une suite de tests API mature couvre plusieurs niveaux. Les tests unitaires sur les serializers vérifient les transformations et validations isolément. Les tests d’intégration sur les viewsets vérifient le comportement HTTP complet (codes statut, structure de réponse, side effects). Les tests de permissions vérifient que chaque endpoint refuse correctement les utilisateurs non autorisés. Pour des APIs critiques métier, des tests de bout en bout via httpx ou requests contre une instance déployée valident le comportement réel y compris middleware et configuration.

Pour ne pas alourdir la suite, cibler 60-70% de couverture sur la logique métier et les cas critiques (parcours d’achat, authentification, validations sensibles), plutôt que 100% sur tout. Les tests qui ne servent qu’à atteindre un nombre n’apportent pas de valeur ; ceux qui attrapent les régressions réelles oui.


12. FAQ

Function-based ou class-based views ?

Class-based (ViewSets) pour les CRUD standards : moins de boilerplate, conventions DRF. Function-based pour les endpoints très spécifiques où la logique custom domine. La plupart des projets utilisent les deux selon le cas.

ModelSerializer ou Serializer simple ?

ModelSerializer dès qu’on travaille avec un modèle Django : configuration automatique des champs, validations héritées du modèle, méthodes create/update fournies. Serializer simple pour des inputs/outputs qui ne correspondent pas à un modèle.

Comment versionner une API DRF ?

Plusieurs stratégies : URL (/api/v1/, /api/v2/), header (Accept-Version: 2), query param. URL est le plus simple à debugger et le plus utilisé. DRF supporte les trois via DEFAULT_VERSIONING_CLASS.

Header (Authorization: Bearer) pour des SPA et apps mobiles. Cookie httpOnly+Secure pour des sites où le frontend est sur le même domaine et où on veut éviter le risque XSS sur le storage. Les deux sont valides selon contexte.

Comment gérer les fichiers uploadés ?

DRF gère nativement multipart. Champ FileField/ImageField dans le serializer. Pour gros fichiers : streaming, ou upload direct vers S3 avec presigned URLs. Pour traitement async : déclencher un job Celery après upload.

Faut-il toujours utiliser DRF ?

Pour des API JSON sérieuses : oui. Pour des endpoints très simples (1-2 routes) : Django seul avec JsonResponse peut suffire. Pour des API ultra-performantes async-first : FastAPI à part. DRF reste la valeur sûre pour la majorité des projets Django.

Comment cacher des endpoints ?

from django.views.decorators.cache import cache_page
from rest_framework.decorators import action

@method_decorator(cache_page(60 * 5), name='list')
class ClientViewSet(viewsets.ModelViewSet):
    ...

Ou cache.set programmatique avec invalidation custom. Attention au cache d’API par utilisateur : nécessite des clés cache spécifiques par user/permission.


Articles liés (cluster Django)


Article mis à jour le 25 avril 2026. Pour signaler une erreur ou suggérer une amélioration, écrivez-nous.

Besoin d'un site web ?

Confiez-nous la Création de Votre Site Web

Site vitrine, e-commerce ou application web — nous transformons votre vision en réalité digitale. Accompagnement personnalisé de A à Z.

À partir de 250.000 FCFA
Parlons de Votre Projet
Publicité