# Django 5 : Créer une API REST avec Django REST Framework > Guide complet pour créer une API REST professionnelle avec Django 5 et DRF. Serializers, ViewSets, authentification JWT et bonnes pratiques expliqués. - Published: 2026-01-15 - Updated: 2026-03-28 - Author: SharpSkill - Tags: django, django rest framework, python, rest api, api development - Reading time: 15 min --- Django REST Framework (DRF) reste la référence incontournable pour créer des APIs REST avec Python. Combiné à Django 5, le framework offre une expérience de développement exceptionnelle avec ses serializers puissants, ses ViewSets automatisés et son système d'authentification flexible. Ce guide couvre la création complète d'une API professionnelle, de l'installation aux tests. > **Django 5 + DRF 3.15** > > Django REST Framework 3.15 apporte le support complet de Django 5, des améliorations de performance significatives et une meilleure intégration avec les types Python natifs. La combinaison reste le choix privilégié pour les APIs Python en production. ## Installation et configuration du projet La mise en place d'un projet Django avec DRF nécessite quelques étapes de configuration. L'utilisation d'un environnement virtuel et d'une structure de projet claire facilite la maintenance à long terme. ```bash # terminal # Création de l'environnement virtuel et installation des dépendances python -m venv venv source venv/bin/activate # Linux/Mac # venv\Scripts\activate # Windows # Installation de Django 5 et DRF pip install django djangorestframework pip install django-filter # Filtrage avancé pip install djangorestframework-simplejwt # Authentification JWT # Création du projet Django django-admin startproject config . python manage.py startapp api ``` Ces commandes créent un projet Django avec une application `api` dédiée aux endpoints REST. ```python # config/settings.py INSTALLED_APPS = [ 'django.contrib.admin', 'django.contrib.auth', 'django.contrib.contenttypes', 'django.contrib.sessions', 'django.contrib.messages', 'django.contrib.staticfiles', # Applications tierces 'rest_framework', 'rest_framework_simplejwt', 'django_filters', # Applications locales 'api', ] REST_FRAMEWORK = { # Classes d'authentification par défaut 'DEFAULT_AUTHENTICATION_CLASSES': [ 'rest_framework_simplejwt.authentication.JWTAuthentication', 'rest_framework.authentication.SessionAuthentication', ], # Permissions par défaut : authentifié requis 'DEFAULT_PERMISSION_CLASSES': [ 'rest_framework.permissions.IsAuthenticated', ], # Pagination globale 'DEFAULT_PAGINATION_CLASS': 'rest_framework.pagination.PageNumberPagination', 'PAGE_SIZE': 20, # Filtres 'DEFAULT_FILTER_BACKENDS': [ 'django_filters.rest_framework.DjangoFilterBackend', 'rest_framework.filters.SearchFilter', 'rest_framework.filters.OrderingFilter', ], } ``` Cette configuration définit l'authentification JWT, la pagination par défaut et les backends de filtrage pour toute l'API. ## Création des modèles de données Les modèles Django représentent la structure des données de l'API. Une conception soignée des modèles simplifie la création des serializers et des vues. ```python # api/models.py from django.db import models from django.contrib.auth.models import AbstractUser from django.core.validators import MinValueValidator, MaxValueValidator import uuid class User(AbstractUser): """Modèle utilisateur personnalisé avec champs supplémentaires.""" id = models.UUIDField( primary_key=True, default=uuid.uuid4, editable=False ) bio = models.TextField(blank=True, max_length=500) avatar = models.URLField(blank=True) created_at = models.DateTimeField(auto_now_add=True) updated_at = models.DateTimeField(auto_now=True) class Meta: ordering = ['-created_at'] def __str__(self): return self.username class Category(models.Model): """Catégorie pour organiser les articles.""" name = models.CharField(max_length=100, unique=True) slug = models.SlugField(max_length=100, unique=True) description = models.TextField(blank=True) created_at = models.DateTimeField(auto_now_add=True) class Meta: verbose_name_plural = 'categories' ordering = ['name'] def __str__(self): return self.name class Article(models.Model): """Article de blog avec relation auteur et catégorie.""" STATUS_CHOICES = [ ('draft', 'Brouillon'), ('published', 'Publié'), ('archived', 'Archivé'), ] id = models.UUIDField( primary_key=True, default=uuid.uuid4, editable=False ) title = models.CharField(max_length=200) slug = models.SlugField(max_length=200, unique=True) content = models.TextField() excerpt = models.TextField(max_length=300, blank=True) # Relation avec l'auteur author = models.ForeignKey( User, on_delete=models.CASCADE, related_name='articles' ) # Relation avec la catégorie category = models.ForeignKey( Category, on_delete=models.SET_NULL, null=True, related_name='articles' ) status = models.CharField( max_length=20, choices=STATUS_CHOICES, default='draft' ) views_count = models.PositiveIntegerField(default=0) 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'] def __str__(self): return self.title ``` L'utilisation d'UUID comme clés primaires améliore la sécurité (identifiants non prédictibles) et facilite la distribution des données. > **Modèle User personnalisé** > > Toujours définir un modèle User personnalisé dès le début du projet, même sans champs supplémentaires. Modifier le modèle User après les migrations initiales est complexe et source d'erreurs. ## Serializers : transformation et validation des données Les serializers constituent le cœur de DRF. Ces classes transforment les objets Python en JSON et inversement, tout en validant les données entrantes. ```python # api/serializers.py from rest_framework import serializers from django.contrib.auth import get_user_model from django.contrib.auth.password_validation import validate_password from .models import Article, Category User = get_user_model() class UserSerializer(serializers.ModelSerializer): """Serializer pour la lecture des données utilisateur.""" # Champ calculé : nombre d'articles publiés articles_count = serializers.SerializerMethodField() class Meta: model = User fields = [ 'id', 'username', 'email', 'bio', 'avatar', 'articles_count', 'created_at' ] # Champs en lecture seule read_only_fields = ['id', 'created_at'] def get_articles_count(self, obj): """Compte les articles publiés de l'utilisateur.""" return obj.articles.filter(status='published').count() class UserCreateSerializer(serializers.ModelSerializer): """Serializer pour la création d'utilisateur avec validation du mot de passe.""" password = serializers.CharField( write_only=True, required=True, validators=[validate_password], style={'input_type': 'password'} ) password_confirm = serializers.CharField( write_only=True, required=True, style={'input_type': 'password'} ) class Meta: model = User fields = [ 'id', 'username', 'email', 'password', 'password_confirm', 'bio', 'avatar' ] def validate(self, attrs): """Vérifie que les deux mots de passe correspondent.""" if attrs['password'] != attrs['password_confirm']: raise serializers.ValidationError({ 'password_confirm': 'Les mots de passe ne correspondent pas.' }) return attrs def create(self, validated_data): """Crée l'utilisateur avec le mot de passe hashé.""" # Retire le champ de confirmation validated_data.pop('password_confirm') # Utilise create_user pour hasher le mot de passe user = User.objects.create_user(**validated_data) return user class CategorySerializer(serializers.ModelSerializer): """Serializer pour les catégories avec compteur d'articles.""" articles_count = serializers.IntegerField( source='articles.count', read_only=True ) class Meta: model = Category fields = ['id', 'name', 'slug', 'description', 'articles_count'] class ArticleListSerializer(serializers.ModelSerializer): """Serializer léger pour la liste des articles.""" # Affiche le username au lieu de l'UUID author = serializers.StringRelatedField() category = serializers.StringRelatedField() class Meta: model = Article fields = [ 'id', 'title', 'slug', 'excerpt', 'author', 'category', 'status', 'views_count', 'published_at', 'created_at' ] class ArticleDetailSerializer(serializers.ModelSerializer): """Serializer complet pour le détail d'un article.""" # Inclut les données complètes de l'auteur author = UserSerializer(read_only=True) category = CategorySerializer(read_only=True) # Champs pour l'écriture (accepte l'ID) category_id = serializers.PrimaryKeyRelatedField( queryset=Category.objects.all(), source='category', write_only=True, required=False ) class Meta: model = Article fields = [ 'id', 'title', 'slug', 'content', 'excerpt', 'author', 'category', 'category_id', 'status', 'views_count', 'published_at', 'created_at', 'updated_at' ] read_only_fields = ['id', 'author', 'views_count', 'created_at', 'updated_at'] def create(self, validated_data): """Assigne automatiquement l'auteur à l'utilisateur connecté.""" validated_data['author'] = self.context['request'].user return super().create(validated_data) ``` La séparation entre `ArticleListSerializer` (léger) et `ArticleDetailSerializer` (complet) optimise les performances en évitant de charger des données inutiles pour les listes. ## ViewSets et routeurs automatiques Les ViewSets regroupent les opérations CRUD dans une seule classe. Combinés aux routeurs, ils génèrent automatiquement les URLs de l'API. ```python # api/views.py from rest_framework import viewsets, status, filters from rest_framework.decorators import action from rest_framework.response import Response from rest_framework.permissions import IsAuthenticated, AllowAny, IsAdminUser from django_filters.rest_framework import DjangoFilterBackend from django.contrib.auth import get_user_model from django.utils import timezone from .models import Article, Category from .serializers import ( UserSerializer, UserCreateSerializer, ArticleListSerializer, ArticleDetailSerializer, CategorySerializer ) from .permissions import IsAuthorOrReadOnly from .filters import ArticleFilter User = get_user_model() class UserViewSet(viewsets.ModelViewSet): """ ViewSet pour la gestion des utilisateurs. Endpoints générés : - GET /users/ : liste des utilisateurs - POST /users/ : création (inscription) - GET /users/{id}/ : détail - PUT/PATCH /users/{id}/ : modification - DELETE /users/{id}/ : suppression - GET /users/me/ : profil de l'utilisateur connecté """ queryset = User.objects.all() filter_backends = [filters.SearchFilter, filters.OrderingFilter] search_fields = ['username', 'email'] ordering_fields = ['created_at', 'username'] def get_serializer_class(self): """Utilise un serializer différent pour la création.""" if self.action == 'create': return UserCreateSerializer return UserSerializer def get_permissions(self): """Permissions dynamiques selon l'action.""" if self.action == 'create': # Inscription ouverte return [AllowAny()] if self.action in ['update', 'partial_update', 'destroy']: # Modification : propriétaire ou admin return [IsAuthenticated()] return [IsAuthenticated()] @action(detail=False, methods=['get']) def me(self, request): """Retourne le profil de l'utilisateur connecté.""" serializer = self.get_serializer(request.user) return Response(serializer.data) @action(detail=False, methods=['patch']) def update_profile(self, request): """Met à jour le profil de l'utilisateur connecté.""" serializer = self.get_serializer( request.user, data=request.data, partial=True ) serializer.is_valid(raise_exception=True) serializer.save() return Response(serializer.data) class CategoryViewSet(viewsets.ModelViewSet): """ ViewSet pour la gestion des catégories. Seuls les admins peuvent créer/modifier/supprimer. """ queryset = Category.objects.all() serializer_class = CategorySerializer lookup_field = 'slug' filter_backends = [filters.SearchFilter] search_fields = ['name', 'description'] def get_permissions(self): """Lecture publique, écriture admin uniquement.""" if self.action in ['list', 'retrieve']: return [AllowAny()] return [IsAdminUser()] class ArticleViewSet(viewsets.ModelViewSet): """ ViewSet pour la gestion des articles. Fonctionnalités : - Filtrage par catégorie, statut, auteur - Recherche textuelle - Tri par date, vues - Actions personnalisées (publication, archivage) """ queryset = Article.objects.select_related('author', 'category') filter_backends = [DjangoFilterBackend, filters.SearchFilter, filters.OrderingFilter] filterset_class = ArticleFilter search_fields = ['title', 'content', 'excerpt'] ordering_fields = ['created_at', 'published_at', 'views_count'] ordering = ['-created_at'] lookup_field = 'slug' def get_serializer_class(self): """Serializer léger pour les listes, complet pour le détail.""" if self.action == 'list': return ArticleListSerializer return ArticleDetailSerializer def get_permissions(self): """Permissions selon l'action.""" if self.action in ['list', 'retrieve']: return [AllowAny()] if self.action == 'create': return [IsAuthenticated()] # Modification/suppression : auteur ou admin return [IsAuthorOrReadOnly()] def get_queryset(self): """Filtre les articles selon l'utilisateur.""" queryset = super().get_queryset() user = self.request.user # Utilisateurs non authentifiés : articles publiés uniquement if not user.is_authenticated: return queryset.filter(status='published') # Admins : tous les articles if user.is_staff: return queryset # Utilisateurs authentifiés : publiés + leurs propres articles from django.db.models import Q return queryset.filter( Q(status='published') | Q(author=user) ) def retrieve(self, request, *args, **kwargs): """Incrémente le compteur de vues à chaque consultation.""" instance = self.get_object() instance.views_count += 1 instance.save(update_fields=['views_count']) serializer = self.get_serializer(instance) return Response(serializer.data) @action(detail=True, methods=['post']) def publish(self, request, slug=None): """Publie un article en brouillon.""" article = self.get_object() if article.status == 'published': return Response( {'error': 'Article déjà publié.'}, status=status.HTTP_400_BAD_REQUEST ) article.status = 'published' article.published_at = timezone.now() article.save() serializer = self.get_serializer(article) return Response(serializer.data) @action(detail=True, methods=['post']) def archive(self, request, slug=None): """Archive un article publié.""" article = self.get_object() article.status = 'archived' article.save() serializer = self.get_serializer(article) return Response(serializer.data) ``` Les actions personnalisées (`@action`) ajoutent des endpoints spécifiques comme `/articles/{slug}/publish/` sans créer de nouvelles vues. ## Permissions personnalisées Les permissions contrôlent l'accès aux ressources. DRF permet de créer des permissions réutilisables pour des règles métier complexes. ```python # api/permissions.py from rest_framework import permissions class IsAuthorOrReadOnly(permissions.BasePermission): """ Permission personnalisée : - Lecture : tout le monde - Écriture : auteur de l'objet ou admin uniquement """ def has_object_permission(self, request, view, obj): # Les méthodes GET, HEAD, OPTIONS sont toujours autorisées if request.method in permissions.SAFE_METHODS: return True # Écriture autorisée uniquement pour l'auteur ou les admins return obj.author == request.user or request.user.is_staff class IsOwnerOrAdmin(permissions.BasePermission): """ Permission pour les ressources utilisateur : - L'utilisateur peut accéder à ses propres ressources - Les admins peuvent accéder à tout """ def has_object_permission(self, request, view, obj): # Vérifie si l'objet est l'utilisateur lui-même if hasattr(obj, 'id') and obj.id == request.user.id: return True # Vérifie si l'objet appartient à l'utilisateur if hasattr(obj, 'user') and obj.user == request.user: return True # Les admins ont accès à tout return request.user.is_staff ``` Ces permissions s'appliquent au niveau de l'objet (`has_object_permission`) pour un contrôle fin sur chaque ressource. ## Filtres personnalisés avec django-filter Les filtres permettent aux clients de l'API de rechercher et filtrer les données selon différents critères. ```python # api/filters.py import django_filters from .models import Article class ArticleFilter(django_filters.FilterSet): """ Filtres personnalisés pour les articles. Exemples d'utilisation : - /articles/?category=tech - /articles/?status=published - /articles/?author=username - /articles/?created_after=2026-01-01 - /articles/?min_views=100 """ # Filtre par slug de catégorie category = django_filters.CharFilter( field_name='category__slug', lookup_expr='exact' ) # Filtre par username de l'auteur author = django_filters.CharFilter( field_name='author__username', lookup_expr='exact' ) # Filtre par date de création (après) created_after = django_filters.DateFilter( field_name='created_at', lookup_expr='gte' ) # Filtre par date de création (avant) created_before = django_filters.DateFilter( field_name='created_at', lookup_expr='lte' ) # Filtre par nombre minimum de vues min_views = django_filters.NumberFilter( field_name='views_count', lookup_expr='gte' ) # Filtre par titre (contient) title = django_filters.CharFilter( field_name='title', lookup_expr='icontains' ) class Meta: model = Article fields = ['status', 'category', 'author'] ``` Ces filtres génèrent automatiquement la documentation dans l'interface browsable de DRF. > **Performance des filtres** > > Les filtres sur des champs textuels avec `icontains` peuvent être lents sur de grandes tables. Pour les recherches full-text, envisagez PostgreSQL avec `SearchVector` ou Elasticsearch. ## Configuration des URLs et routeurs Le routeur DRF génère automatiquement les URLs RESTful à partir des ViewSets enregistrés. ```python # api/urls.py from django.urls import path, include from rest_framework.routers import DefaultRouter from rest_framework_simplejwt.views import ( TokenObtainPairView, TokenRefreshView, TokenVerifyView, ) from .views import UserViewSet, CategoryViewSet, ArticleViewSet # Création du routeur avec génération automatique des URLs router = DefaultRouter() router.register(r'users', UserViewSet, basename='user') router.register(r'categories', CategoryViewSet, basename='category') router.register(r'articles', ArticleViewSet, basename='article') urlpatterns = [ # URLs générées par le routeur path('', include(router.urls)), # Endpoints d'authentification JWT path('auth/token/', TokenObtainPairView.as_view(), name='token_obtain_pair'), path('auth/token/refresh/', TokenRefreshView.as_view(), name='token_refresh'), path('auth/token/verify/', TokenVerifyView.as_view(), name='token_verify'), ] ``` ```python # config/urls.py from django.contrib import admin from django.urls import path, include urlpatterns = [ path('admin/', admin.site.urls), # Toutes les URLs de l'API sous /api/ path('api/', include('api.urls')), ] ``` Cette configuration expose les endpoints suivants : - `POST /api/auth/token/` : obtention du token JWT - `POST /api/auth/token/refresh/` : rafraîchissement du token - `GET/POST /api/users/` : liste et création d'utilisateurs - `GET/PUT/PATCH/DELETE /api/users/{id}/` : opérations sur un utilisateur - Et ainsi de suite pour les catégories et articles. ## Configuration JWT avancée L'authentification JWT nécessite une configuration adaptée à la sécurité et à l'expérience utilisateur. ```python # config/settings.py from datetime import timedelta SIMPLE_JWT = { # Durée de validité du token d'accès 'ACCESS_TOKEN_LIFETIME': timedelta(minutes=30), # Durée de validité du token de rafraîchissement 'REFRESH_TOKEN_LIFETIME': timedelta(days=7), # Rotation automatique des tokens de rafraîchissement 'ROTATE_REFRESH_TOKENS': True, # Blacklist les anciens tokens après rotation 'BLACKLIST_AFTER_ROTATION': True, # Algorithme de signature 'ALGORITHM': 'HS256', # Clé de signature (utiliser une clé secrète en production) 'SIGNING_KEY': SECRET_KEY, # Préfixe du header Authorization 'AUTH_HEADER_TYPES': ('Bearer',), # Champs inclus dans le token 'USER_ID_FIELD': 'id', 'USER_ID_CLAIM': 'user_id', } ``` La rotation des tokens (`ROTATE_REFRESH_TOKENS`) renforce la sécurité en invalidant les anciens tokens après chaque rafraîchissement. ## Tests automatisés de l'API DRF fournit des outils de test intégrés pour valider le comportement de l'API. ```python # api/tests/test_articles.py from django.test import TestCase from django.urls import reverse from rest_framework.test import APIClient from rest_framework import status from django.contrib.auth import get_user_model from api.models import Article, Category User = get_user_model() class ArticleAPITestCase(TestCase): """Tests pour les endpoints articles.""" def setUp(self): """Préparation des données de test.""" self.client = APIClient() # Création d'un utilisateur de test self.user = User.objects.create_user( username='testuser', email='test@example.com', password='testpass123' ) # Création d'une catégorie self.category = Category.objects.create( name='Tech', slug='tech', description='Articles technologiques' ) # Création d'un article publié self.article = Article.objects.create( title='Test Article', slug='test-article', content='Contenu de test détaillé.', excerpt='Résumé court', author=self.user, category=self.category, status='published' ) def test_list_articles_unauthenticated(self): """Les articles publiés sont accessibles sans authentification.""" url = reverse('article-list') response = self.client.get(url) self.assertEqual(response.status_code, status.HTTP_200_OK) self.assertEqual(len(response.data['results']), 1) def test_list_articles_filters_drafts(self): """Les brouillons ne sont pas visibles pour les utilisateurs non authentifiés.""" # Création d'un brouillon Article.objects.create( title='Draft Article', slug='draft-article', content='Brouillon', author=self.user, status='draft' ) url = reverse('article-list') response = self.client.get(url) # Seul l'article publié est visible self.assertEqual(len(response.data['results']), 1) def test_create_article_authenticated(self): """Un utilisateur authentifié peut créer un article.""" self.client.force_authenticate(user=self.user) url = reverse('article-list') data = { 'title': 'Nouvel Article', 'slug': 'nouvel-article', 'content': 'Contenu du nouvel article.', 'category_id': self.category.id, } response = self.client.post(url, data, format='json') self.assertEqual(response.status_code, status.HTTP_201_CREATED) self.assertEqual(Article.objects.count(), 2) # L'auteur est automatiquement assigné self.assertEqual( Article.objects.get(slug='nouvel-article').author, self.user ) def test_create_article_unauthenticated(self): """Un utilisateur non authentifié ne peut pas créer d'article.""" url = reverse('article-list') data = { 'title': 'Article Non Autorisé', 'slug': 'article-non-autorise', 'content': 'Contenu', } response = self.client.post(url, data, format='json') self.assertEqual(response.status_code, status.HTTP_401_UNAUTHORIZED) def test_update_own_article(self): """Un auteur peut modifier son propre article.""" self.client.force_authenticate(user=self.user) url = reverse('article-detail', kwargs={'slug': self.article.slug}) data = {'title': 'Titre Modifié'} response = self.client.patch(url, data, format='json') self.assertEqual(response.status_code, status.HTTP_200_OK) self.article.refresh_from_db() self.assertEqual(self.article.title, 'Titre Modifié') def test_update_other_user_article(self): """Un utilisateur ne peut pas modifier l'article d'un autre.""" other_user = User.objects.create_user( username='other', email='other@example.com', password='otherpass123' ) self.client.force_authenticate(user=other_user) url = reverse('article-detail', kwargs={'slug': self.article.slug}) data = {'title': 'Titre Modifié'} response = self.client.patch(url, data, format='json') self.assertEqual(response.status_code, status.HTTP_403_FORBIDDEN) def test_publish_action(self): """L'action publish change le statut de l'article.""" draft = Article.objects.create( title='Brouillon', slug='brouillon', content='Contenu brouillon', author=self.user, status='draft' ) self.client.force_authenticate(user=self.user) url = reverse('article-publish', kwargs={'slug': draft.slug}) response = self.client.post(url) self.assertEqual(response.status_code, status.HTTP_200_OK) draft.refresh_from_db() self.assertEqual(draft.status, 'published') self.assertIsNotNone(draft.published_at) def test_filter_by_category(self): """Le filtrage par catégorie fonctionne correctement.""" url = reverse('article-list') response = self.client.get(url, {'category': 'tech'}) self.assertEqual(response.status_code, status.HTTP_200_OK) self.assertEqual(len(response.data['results']), 1) def test_search_articles(self): """La recherche textuelle fonctionne.""" url = reverse('article-list') response = self.client.get(url, {'search': 'Test'}) self.assertEqual(response.status_code, status.HTTP_200_OK) self.assertEqual(len(response.data['results']), 1) ``` L'exécution des tests se fait via la commande `python manage.py test api.tests`. ## Gestion des erreurs et réponses standardisées Une gestion cohérente des erreurs améliore l'expérience développeur pour les consommateurs de l'API. ```python # api/exceptions.py from rest_framework.views import exception_handler from rest_framework.response import Response from rest_framework import status def custom_exception_handler(exc, context): """ Gestionnaire d'exceptions personnalisé pour standardiser les réponses d'erreur. Format de réponse : { "success": false, "error": { "code": "ERROR_CODE", "message": "Description de l'erreur", "details": {...} # Optionnel } } """ # Appelle le gestionnaire par défaut response = exception_handler(exc, context) if response is not None: # Standardise le format de réponse custom_response = { 'success': False, 'error': { 'code': get_error_code(exc), 'message': get_error_message(response.data), 'details': response.data if isinstance(response.data, dict) else None } } response.data = custom_response return response def get_error_code(exc): """Retourne un code d'erreur basé sur le type d'exception.""" error_codes = { 'ValidationError': 'VALIDATION_ERROR', 'AuthenticationFailed': 'AUTHENTICATION_FAILED', 'NotAuthenticated': 'NOT_AUTHENTICATED', 'PermissionDenied': 'PERMISSION_DENIED', 'NotFound': 'NOT_FOUND', 'MethodNotAllowed': 'METHOD_NOT_ALLOWED', 'Throttled': 'RATE_LIMIT_EXCEEDED', } return error_codes.get(exc.__class__.__name__, 'UNKNOWN_ERROR') def get_error_message(data): """Extrait un message d'erreur lisible des données de réponse.""" if isinstance(data, dict): if 'detail' in data: return str(data['detail']) # Collecte les messages de validation messages = [] for field, errors in data.items(): if isinstance(errors, list): messages.extend([f"{field}: {e}" for e in errors]) else: messages.append(f"{field}: {errors}") return '; '.join(messages) if messages else 'Erreur de validation' return str(data) ``` ```python # config/settings.py REST_FRAMEWORK = { # ... autres configurations 'EXCEPTION_HANDLER': 'api.exceptions.custom_exception_handler', } ``` ## Conclusion Django REST Framework combiné à Django 5 offre un écosystème complet pour créer des APIs REST professionnelles. La puissance des serializers, la flexibilité des ViewSets et l'intégration native de l'authentification JWT permettent de construire rapidement des APIs robustes et sécurisées. ### Checklist pour une API Django de qualité - ✅ Utiliser des serializers séparés pour lecture et écriture - ✅ Implémenter des permissions personnalisées pour le contrôle d'accès - ✅ Configurer des filtres pour la recherche et le tri - ✅ Ajouter l'authentification JWT avec rotation des tokens - ✅ Écrire des tests unitaires pour chaque endpoint - ✅ Standardiser le format des réponses d'erreur - ✅ Documenter l'API via DRF Spectacular ou drf-yasg L'approche de DRF encourage la réutilisation et la composition : les serializers, permissions et filtres s'assemblent pour créer des APIs maintenables à long terme. La documentation automatique et l'interface browsable accélèrent le développement et facilitent l'intégration pour les équipes frontend. --- Source: SharpSkill (https://sharpskill.dev), tech interview preparation for your real stack. HTML version of this page: https://sharpskill.dev/fr/blog/django/django-5-creer-api-rest-drf