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.

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 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.
# 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 apiCes commandes créent un projet Django avec une application api dédiée aux endpoints REST.
# 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.
# 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.titleL'utilisation d'UUID comme clés primaires améliore la sécurité (identifiants non prédictibles) et facilite la distribution des données.
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.
# 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.
Prêt à réussir tes entretiens Django ?
Entraîne-toi avec nos simulateurs interactifs, fiches express et tests techniques.
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.
# 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.
# 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_staffCes 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.
# 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.
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.
# 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'),
]# 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 JWTPOST /api/auth/token/refresh/: rafraîchissement du tokenGET/POST /api/users/: liste et création d'utilisateursGET/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.
# 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.
# 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.
# 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)# 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
Passe à la pratique !
Teste tes connaissances avec nos simulateurs d'entretien et tests techniques.
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.
Tags
Partager
Articles similaires

Questions d'entretien Django et Python : Top 25 en 2026
Les 25 questions d'entretien Django et Python les plus posées. ORM, vues, middleware, DRF, signaux et optimisation avec réponses détaillées et exemples de code.

Django 5.2 : Middleware Personnalisé et Gestion des Signaux pour les Entretiens Techniques
Guide complet sur les middleware personnalisés et les signaux dans Django 5.2. Exemples pratiques de middleware de logging, middleware asynchrone, signaux pre_save/post_save et questions fréquentes en entretien.

Django ORM : Optimiser vos requêtes pour des performances maximales
Guide complet pour optimiser les requêtes Django ORM. select_related, prefetch_related, index, analyse N+1 et techniques avancées pour des applications performantes.