BROUILLON, ARTICLE EN COURS DE REDACTION (CORRIGER LES FAUTES ETC...)
J'ai récemment publié un article : Créer une API REST en Python. Dans ce guide, nous allons nous intéresser à Django Rest Framework (on l'appellera DRF), largement utilisé pour le développement d'API. Et si vous connaissez déjà Django, 80% du travail est déjà fait. 😎
Mais pourquoi utiliser Django Rest Framework plutôt que Django et ses templates ?
Vous pourriez avoir besoin de communiquer avec un front-end Vue JS ou React, ou de communiquer avec d'autres applications.
Qu'est-ce que DRF ?
Il s'agit d'une surcouche de Django extrêmement complète. DRF utilise donc l'ORM de Django, l'administration, la sécurité, les modèles... tout en y ajoutant ce qu'il faut pour créer une API.
Dans ce guide, nous allons développer une API pour illustrer les différents concepts.
Les fonctionnalités phares
-
DRF génère une interface web qui permet de tester son API directement dans le navigateur.
-
DRF fournit des classes de sérialisation afin de convertir les objets Python au format JSON et vice-versa.
-
DRF intègre les fonctionnalités de Django et permet aussi d'utiliser l'authentification via des tokens JWT.
-
DRF met à disposition des décorateurs ou des classes pour créer des vues d'API.
Le projet fil conducteur
Nous allons construire l'API d'un système de gestion de bibliothèque. C'est un exemple assez simple et pratique pour comprendre les différents concepts que nous allons aborder.
Les bases du projet
Tout d'abord, j'ai créé un dossier avec un environnement virtuel dans lequel j'ai installé DRF : pip install djangorestframework. Si Django n'est pas encore installé, il le sera automatiquement.
J'ai ensuite initialisé un projet avec django-admin startproject project ., puis généré une application library avec python manage.py startapp library.
Nous utiliserons ces modèles :
from django.db import models from django.contrib.auth import get_user_model User = get_user_model() class Author(models.Model): name = models.CharField(max_length=255) def __str__(self): return self.name class Book(models.Model): title = models.CharField(max_length=255) author = models.ForeignKey(Author, related_name='books', on_delete=models.CASCADE) def __str__(self): return self.title class Loan(models.Model): user = models.ForeignKey(User, on_delete=models.CASCADE) book = models.ForeignKey(Book, on_delete=models.CASCADE) borrow_date = models.DateField(auto_now_add=True) return_date = models.DateField(null=True, blank=True) def __str__(self): return f"{self.user.username} a emprunté {self.book.title}"
Il faut aussi penser à enregistrer les modèles dans le fichier admin.py :
from django.contrib import admin from .models import Author, Book, Loan admin.site.register(Author) admin.site.register(Book) admin.site.register(Loan)
Il faut penser à ajouter notre application et DRF dans le fichier settings.py :
INSTALLED_APPS = [ 'django.contrib.admin', 'django.contrib.auth', 'django.contrib.contenttypes', 'django.contrib.sessions', 'django.contrib.messages', 'django.contrib.staticfiles', # DRF et application library 'rest_framework', 'library', ]
Il ne reste plus qu'à exécuter les migrations et à créer un super-utilisateur :
python manage.py makemigrations python manage.py migrate python manage.py createsuperuser
Les Serializers
Les serializers sont des classes très puissantes qui vous permettent de :
-
Désérialiser des données : elles transforment les données JSON en objets Python
-
Sérialiser les données : elles transforment les objets Python en JSON
-
Valider les données
-
Modifier ou enrichir les données avant l'envoi
La classe Serializer
C'est la classe de base qui vous oblige à redéfinir chaque champ. Créez le fichier serializers.py dans l'application library, puis codez votre premier serializer :
from rest_framework import serializers from .models import Author class AuthorSerializer(serializers.Serializer): id = serializers.IntegerField(read_only=True) name = serializers.CharField(max_length=255) def create(self, validated_data): return Author.objects.create(**validated_data) def update(self, instance, validated_data): instance.name = validated_data.get("name", instance.name) instance.save() return instance
Les champs définissent le type et les contraintes des données attendues par le serializer, ce qui permet une validation automatique avant traitement. Les méthodes create et update permettent de définir les logiques de création et de mise à jour pour Author.
Utilisation dans une vue de liste
Pour illustrer notre serializer, nous allons l'utiliser dans une vue très simple. Nous reviendrons sur les vues plus tard. Dans le fichier views.py :
from rest_framework.decorators import api_view from rest_framework.response import Response from rest_framework import status from .models import Author from .serializers import AuthorSerializer @api_view(["GET", "POST"]) def author_list_and_create(request): if request.method == "GET": authors = Author.objects.all() serializer = AuthorSerializer(authors, many=True) return Response(serializer.data) elif request.method == "POST": serializer = AuthorSerializer(data=request.data) if serializer.is_valid(): serializer.save() return Response(serializer.data, status=status.HTTP_201_CREATED) return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST)
Nous avons créé une vue d'API qui permet d'utiliser les méthodes GET et POST : @api_view(["GET", "POST"]).
-
La méthode GET récupère tous les auteurs de la base de données, les sérialise (
many=Truepour une liste d'objets) et retourne les données avec un statut 200 par défaut -
La méthode POST désérialise les données reçues, les valide avec le serializer, puis les sauvegarde en base si elles sont valides avec un statut 201 ou retourne les erreurs avec un statut 400. La méthode
serializer.save()appelle automatiquement la méthodecreatedu serializer
Maintenant, dans urls.py, ajoutons une URL pour utiliser notre vue. Pour rester simple, nous utilisons le urls.py du dossier project :
from django.contrib import admin from django.urls import path from library.views import author_list_and_create urlpatterns = [ path('admin/', admin.site.urls), path('authors/', author_list_and_create, name='author-list-create'), ]
Maintenant, allons dans l'administration Django pour créer deux auteurs.
Création des auteurs dans l'administration
Maintenant, vous pouvez lancer le serveur avec python manage.py runserver et aller sur cette URL : http://127.0.0.1:8000/authors/.
Récupération des auteurs avec la méthode GET
[ { "id": 1, "name": "Patrick" }, { "id": 2, "name": "Sebastien" } ]
Nous récupérons bien les deux auteurs avec la méthode GET. Mais comme nous avons autorisé la méthode POST, vous remarquerez qu'il est aussi possible d'envoyer des données.
Champ pour envoyer des données
Maintenant, nous pouvons créer un nouvel auteur :
Données pour créer le nouvel auteur
Réponse après la création
On remarque bien une réponse 201 avec notre auteur.
{ "id": 3, "name": "Gabriel" }
Utilisation dans une vue de détail
Maintenant, nous allons faire travailler la méthode update de notre serializer tout en ajoutant une vue de détail :
# views.py @api_view(["GET", "PUT", "DELETE"]) def author_detail(request, pk): try: author = Author.objects.get(pk=pk) except Author.DoesNotExist: return Response(status=status.HTTP_404_NOT_FOUND) if request.method == "GET": serializer = AuthorSerializer(author) return Response(serializer.data) elif request.method == "PUT": serializer = AuthorSerializer(author, data=request.data) if serializer.is_valid(): serializer.save() return Response(serializer.data) return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST) elif request.method == "DELETE": author.delete() return Response(status=status.HTTP_204_NO_CONTENT)
Pensez à ajouter l'URL : path('author/<int:pk>/', author_detail, name='author-detail'),.
Cette vue va gérer les opérations sur un auteur spécifique. Il est identifié par sa clé primaire. On essaie de le récupérer dans la base de données ; en cas d'échec, on retourne une erreur 404.
Ensuite, plusieurs actions sont possibles selon la méthode HTTP :
-
GET pour sérialiser l'auteur trouvé avec un statut 200 implicite
-
PUT pour mettre à jour l'auteur avec
serializer.save()qui appelle la méthodeupdate, car une instance est fournie contrairement à POST. Si les données sont invalides, on retourne les erreurs avec un statut 400 -
DELETE pour supprimer l'auteur de la base de données et retourner un statut 204
De manière générale, il est préférable de spécifier le statut lorsqu'il est différent de 200.
Il est temps de se rendre sur notre URL pour récupérer l'auteur avec la clé primaire 1 : http://127.0.0.1:8000/author/1/.
Vue de détail
On récupère bien les informations et nous avons en plus les boutons DELETE et PUT. Je vais modifier le nom de Patrick en Patrick le Patoche.
Modification du nom
À noter
Ne cherchez pas à modifier l'ID, Django ignorera cette modification.
Tant qu'à faire, autant tester la suppression :
Suppression de Patrick le Patoche
Les méthodes de validation
Avant que les données ne soient sauvegardées en base de données, il est possible de les contrôler via le système de validation intégré aux serializers. Pour ceux qui utilisent déjà Django et les formulaires, vous ferez vite le rapprochement avec la méthode clean_nom_du_champ.
En effet, il est possible de définir une méthode pour contrôler un champ spécifique. On voudrait, par exemple, s'assurer qu'un nom d'auteur commence obligatoirement par une majuscule :
class AuthorSerializer(serializers.Serializer): id = serializers.IntegerField(read_only=True) name = serializers.CharField(max_length=255) def create(self, validated_data): return Author.objects.create(**validated_data) def update(self, instance, validated_data): instance.name = validated_data.get("name", instance.name) instance.save() return instance def validate_name(self, value): if not value[0].isupper(): raise serializers.ValidationError("Le nom doit commencer par une majuscule.") return value
Si j'essaie de créer un utilisateur de cette manière via l'URL http://127.0.0.1:8000/authors/ :
{ "name": "robert" }
J'ai bien une erreur de validation avec, en clé, le nom du champ et, en valeur, une liste avec mon erreur :
{ "name": [ "Le nom doit commencer par une majuscule." ] }
Nous verrons qu'il est possible de valider plusieurs champs ensemble avec la méthode validate, ce qui peut être pratique si la validité d'un champ dépend d'un autre. Nous verrons un exemple un peu plus tard avec un ModelSerializer.
La classe ModelSerializer
Lorsque nous travaillons avec des modèles, redéfinir chaque champ est fastidieux. DRF propose la classe ModelSerializer, qui génère automatiquement les champs à partir d'un modèle Django.
À noter
Les méthodes de validation que nous avons vues précédemment fonctionnent également avec ModelSerializer.
Nous allons maintenant refactoriser notre sérialiseur :
class AuthorSerializer(serializers.ModelSerializer): class Meta: model = Author fields = '__all__' # On pourrait spécifier une liste de champs au lieu de '__all__' # fields = ['id', 'name'] # On peut aussi utiliser exclude pour exclure certains champs # exclude = ['mon_champ']
DRF génère automatiquement les champs avec leurs types et contraintes en fonction du modèle. Vous n'avez plus besoin de définir les méthodes create et update ; c'est géré automatiquement 😎.
Toujours dans serializers.py, nous allons maintenant créer des sérialiseurs pour Book et Loan. Pour avoir un exemple très complet, nous allons définir une méthode de validation pour Loan.
class BookSerializer(serializers.ModelSerializer): class Meta: model = Book fields = ['id', 'title', 'author']
class LoanSerializer(serializers.ModelSerializer): class Meta: model = Loan fields = ['id', 'user', 'book', 'borrow_date', 'return_date'] def validate(self, data): # Interdire de créer un emprunt avec une date de retour if self.instance is None and data.get("return_date"): raise serializers.ValidationError("Impossible de créer un emprunt déjà retourné.") # Vérifier qu'on ne peut pas emprunter un livre déjà emprunté (uniquement à la création) if self.instance is None: existing_loan = Loan.objects.filter(book=data.get("book"), return_date__isnull=True).exists() if existing_loan: raise serializers.ValidationError("Ce livre est déjà emprunté.") # Vérifier que la date de retour est après la date d'emprunt (uniquement en UPDATE) if self.instance and data.get("return_date"): if data["return_date"] < self.instance.borrow_date: raise serializers.ValidationError("La date de retour doit être après la date d'emprunt.") return data
Ici, nous implémentons la méthode validate pour :
-
Refuser qu'un emprunt soit créé avec une
return_dateà la création -
Vérifier qu'un livre n'est pas déjà emprunté (à la création)
-
Vérifier qu'en cas de modification, la date de retour est cohérente avec la date d'emprunt
À noter
Le champ borrow_date est automatiquement passé en lecture seule, car c’est un champ avec auto_now_add=True : borrow_date = models.DateField(auto_now_add=True).
Les champs personnalisés avec SerializerMethodField
Parfois, on peut vouloir ajouter des champs calculés qui n'existent pas dans le modèle. Par exemple, en ajoutant une méthode pour afficher le statut d'un emprunt :
class LoanSerializer(serializers.ModelSerializer): status = serializers.SerializerMethodField() class Meta: model = Loan fields = ['id', 'user', 'book', 'borrow_date', 'return_date', 'status'] def get_status(self, obj): return "Retourné" if obj.return_date else "En cours" def validate(self, data): # Méthode existante
Pour illustrer notre LoanSerializer, il va falloir créer une petite vue très simple pour l'exemple :
# views.py # Ne pas oublier les imports si besoin @api_view(["GET"]) def loan_list(request): loans = Loan.objects.all() serializer = LoanSerializer(loans, many=True) return Response(serializer.data)
# urls.py from django.contrib import admin from django.urls import path from library.views import author_list_and_create, author_detail, loan_list urlpatterns = [ path('admin/', admin.site.urls), path('authors/', author_list_and_create, name='author-list-create'), path('author/<int:pk>/', author_detail, name='author-detail'), path('loans/', loan_list, name='loan-list'), ]
Avant de continuer, nous allons créer un livre et un emprunt dans l'administration Django :
Création d'un livre dans l'administration Django
Création d'un emprunt dans l'administration Django
Maintenant, si je me rends sur http://127.0.0.1:8000/loans/, je peux voir le statut de l'emprunt grâce à notre SerializerMethodField.
Affichage de l'emprunt via l'API
Une propriété dans le modèle
Dans ce cas, plutôt que d'avoir un SerializerMethodField, il serait préférable de définir ce type de logique dans le modèle avec une @property :
class Loan(models.Model): user = models.ForeignKey(User, on_delete=models.CASCADE) book = models.ForeignKey(Book, on_delete=models.CASCADE) borrow_date = models.DateField(auto_now_add=True) return_date = models.DateField(null=True, blank=True) @property def status(self): return "Retourné" if self.return_date else "En cours" def __str__(self): return f"{self.user.username} a emprunté {self.book.title}"
On peut maintenant simplifier le sérialiseur en spécifiant juste 'status' dans fields :
class LoanSerializer(serializers.ModelSerializer): class Meta: model = Loan fields = ['id', 'user', 'book', 'borrow_date', 'return_date', 'status'] def validate(self, data): # Méthode existante
En retournant sur http://127.0.0.1:8000/loans/, on aura exactement le même résultat. DRF détecte les @property et les sérialise, pratique non ?!
À noter
Quand la logique est liée à la logique métier du modèle ou que l'on veut réutiliser la méthode ailleurs, une @property est plus appropriée.
Par contre, pour un usage plus temporaire ou quand on a besoin du contexte de la requête, on préférera un SerializerMethodField.
À vous de jouer
Voici comment récupérer la requête dans les sérialiseurs (avec une méthode fictive) :
class AuthorSerializer(serializers.ModelSerializer): foo= serializers.SerializerMethodField() class Meta: model = Author fields = '__all__' def get_foo(self, obj): request = self.context.get('request') if request.user.is_staff: # logique pour les utilisateurs staff # logique pour les autres utilisateurs
Voici comment récupérer la requête dans les sérialiseurs (avec une méthode fictive) :
@api_view(["GET", "POST"]) def author_list_and_create(request): if request.method == "GET": authors = Author.objects.all() serializer = AuthorSerializer(authors, many=True, context={'request': request}) return Response(serializer.data)
Avant de passer à la suite, essayez d'implémenter vous-même un SerializerMethodField qui permet de savoir si j'ai emprunté un livre. Sinon, passez directement à la suite, nous allons le faire ensemble.
Commençons par implémenter une méthode dans le BookSerializer :
class BookSerializer(serializers.ModelSerializer): is_borrowed_by_me = serializers.SerializerMethodField() class Meta: model = Book fields = ['id', 'title', 'author', 'is_borrowed_by_me'] def get_is_borrowed_by_me(self, obj): request = self.context.get("request") if request and request.user.is_authenticated: return Loan.objects.filter(book=obj, user=request.user, return_date__isnull=True).exists() return False
Puis ajoutez une vue simple pour afficher les livres, tout en passant la requête au sérialiseur :
# views.py # Penser aux imports manquants si besoin @api_view(["GET"]) def book_list(request): books = Book.objects.all() serializer = BookSerializer(books, many=True, context={'request': request}) return Response(serializer.data)
On ajoute la vue à urls.py :
# urls.py from django.contrib import admin from django.urls import path from library.views import author_list_and_create, author_detail, loan_list, book_list urlpatterns = [ path('admin/', admin.site.urls), path('authors/', author_list_and_create, name='author-list-create'), path('author/<int:pk>/', author_detail, name='author-detail'), path('loans/', loan_list, name='loan-list'), path('books/', book_list, name='book-list'), ]
J'avais déjà un emprunt en cours et j'ai ajouté un livre dans l'administration. Quand je me rends sur l'URL http://127.0.0.1:8000/books/ :
Vue d'API pour les livres
Gérer les relations entre modèles
Le modèle Book a une relation ForeignKey vers Author, et DRF permet de représenter cette relation dans l'API de différentes manières.
Le comportement par défaut
Actuellement, avec le BookSerializer, nous affichons l'ID de l'auteur :
Représentation d'un livre par défaut
Léger, rapide et facile à utiliser pour créer ou modifier un livre.
PrimaryKeyRelatedField pour plus de contrôle
Comme vu précédemment, le serializer génère automatiquement la clé primaire de l'auteur. Cependant, il est possible de la définir explicitement pour avoir plus de contrôle :
class BookSerializer(serializers.ModelSerializer): author = serializers.PrimaryKeyRelatedField(read_only=True) class Meta: model = Book fields = ['id', 'title', 'author', 'is_borrowed_by_me']
read_only permet de rendre le champ en lecture seule.
class BookSerializer(serializers.ModelSerializer): author = serializers.PrimaryKeyRelatedField(queryset=Author.objects.filter(is_active=True)) is_borrowed_by_me = serializers.SerializerMethodField() class Meta: model = Book fields = ['id', 'title', 'author', 'is_borrowed_by_me']
Bien qu'en réalité nous n'ayons pas de champ is_active dans le modèle Author, si ce champ était présent, on pourrait définir quels auteurs sont autorisés lors de la création ou modification du livre. Le queryset ne filtre pas les livres, mais bien les auteurs.
Afficher le nom de l'auteur
Il est possible d'utiliser ce que l'on appelle un StringRelatedField, qui utilise la représentation en chaîne de caractères via la méthode __str__ de Author :
class BookSerializer(serializers.ModelSerializer): author = serializers.StringRelatedField() is_borrowed_by_me = serializers.SerializerMethodField() class Meta: model = Book fields = ['id', 'title', 'author', 'is_borrowed_by_me'] def get_is_borrowed_by_me(self, obj): # Méthode existante
Affichage du nom des auteurs
Lisible, mais en lecture seule uniquement, vous ne pourrez pas créer de livres de cette manière.
Objet complet, le Nested Serializer
Certainement l'approche la plus pratique : utiliser un serializer pour la lecture et un autre pour l'écriture.
On utilisera un serializer pour la lecture :
class BookListSerializer(serializers.ModelSerializer): author = AuthorSerializer(read_only=True) is_borrowed_by_me = serializers.SerializerMethodField() class Meta: model = Book fields = ['id', 'title', 'author', 'is_borrowed_by_me'] def get_is_borrowed_by_me(self, obj): request = self.context.get("request") if request and request.user.is_authenticated: return Loan.objects.filter(book=obj, user=request.user, return_date__isnull=True).exists() return False
Ici, nous utilisons le serializer AuthorSerializer dans le BookListSerializer. Modifions la vue :
@api_view(["GET"]) def book_list(request): books = Book.objects.all() serializer = BookListSerializer(books, many=True, context={'request': request}) return Response(serializer.data)
Affichage de l'auteur de manière imbriquée
Parallèlement, on aurait un serializer pour la création ou la modification :
class BookCreateUpdateSerializer(serializers.ModelSerializer): class Meta: model = Book fields = ['id', 'title', 'author']
Et dans la vue, on utiliserait les deux serializers en fonction de la méthode employée :
# views.py @api_view(["GET", "POST"]) def book_list(request): if request.method == "GET": books = Book.objects.all() serializer = BookListSerializer(books, many=True, context={'request': request}) return Response(serializer.data) elif request.method == "POST": serializer = BookCreateUpdateSerializer(data=request.data) if serializer.is_valid(): serializer.save() return Response(serializer.data, status=status.HTTP_201_CREATED) return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST)
Les vues
DRF propose plusieurs niveaux d'abstraction concernant les vues. Nous avons déjà évoqué les vues avec le décorateur @api_view pour illustrer les serializers. C'est simple, mais cela implique pas mal de code répétitif. Nous ne reviendrons pas dessus (n'hésitez pas à consulter de nouveau les @api_view que nous avons codés précédemment).
La classe APIView
Ce n'est que mon avis personnel, mais quitte à redéfinir mes méthodes HTTP, autant utiliser l'APIView. Elle est très semblable à ce que l'on a fait précédemment, mais avec une classe, ce qui donne tout de suite un côté plus organisé à votre code.
Commençons par remplacer notre ancienne vue author_list_and_create.
# views.py # Nouvel import from rest_framework.views import APIView class AuthorListCreateView(APIView): def get(self, request): authors = Author.objects.all() serializer = AuthorSerializer(authors, many=True) return Response(serializer.data) def post(self, request): serializer = AuthorSerializer(data=request.data) if serializer.is_valid(): serializer.save() return Response(serializer.data, status=status.HTTP_201_CREATED) return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST)
Mettons à jour le fichier urls.py :
from django.contrib import admin from django.urls import path from library.views import author_detail, loan_list, book_list, AuthorListCreateView urlpatterns = [ path('admin/', admin.site.urls), path('authors/', AuthorListCreateView.as_view(), name='author-list-create'), path('author/<int:pk>/', author_detail, name='author-detail'), path('loans/', loan_list, name='loan-list'), path('books/', book_list, name='book-list'), ]
Et pour la vue de détail :
# Nouvel import from django.shortcuts import get_object_or_404 class AuthorDetailView(APIView): def get(self, request, pk): author = get_object_or_404(Author, pk=pk) serializer = AuthorSerializer(author) return Response(serializer.data) def put(self, request, pk): author = get_object_or_404(Author, pk=pk) serializer = AuthorSerializer(author, data=request.data) if serializer.is_valid(): serializer.save() return Response(serializer.data) return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST) def delete(self, request, pk): author = get_object_or_404(Author, pk=pk) author.delete() return Response(status=status.HTTP_204_NO_CONTENT)
Puis, mettons à jour urls.py :
from django.contrib import admin from django.urls import path from library.views import loan_list, book_list, AuthorListCreateView, AuthorDetailView urlpatterns = [ path('admin/', admin.site.urls), path('authors/', AuthorListCreateView.as_view(), name='author-list-create'), path('author/<int:pk>/', AuthorDetailView.as_view(), name='author-detail'), path('loans/', loan_list, name='loan-list'), path('books/', book_list, name='book-list'), ]
Le code est déjà un peu mieux organisé, mais il reste encore beaucoup de redondances. Passons à la suite.
Les vues génériques
DRF propose des vues permettant de réduire la quantité de code. Commençons par importer generics depuis rest_framework dans views.py.
ListCreateAPIView
Remplaçons notre AuthorListCreateView par :
class AuthorListCreateView(generics.ListCreateAPIView): queryset = Author.objects.all() serializer_class = AuthorSerializer
DRF gère automatiquement :
-
La récupération de la liste
-
La sérialisation
-
La création avec validation
-
Les codes de statut
De plus, lorsque vous accédez à l'endpoint via le navigateur, DRF affiche un formulaire avec le nom des champs du modèle !
Formulaire pré-rempli
RetrieveUpdateDestroyAPIView
Vous vous doutez que nous allons maintenant aborder la vue de détail :
class AuthorDetailView(generics.RetrieveUpdateDestroyAPIView): queryset = Author.objects.all() serializer_class = AuthorSerializer
Cette classe permet de gérer les méthodes GET, PUT, PATCH et DELETE sur un objet spécifique.
Je vous invite à consulter cette partie de la documentation de DRF.
Page de la documentation
Personnalisation des vues génériques
Il est très simple de personnaliser ces vues. On pourrait, par exemple, utiliser différents sérialiseurs selon l'action :
class BookListCreateView(generics.ListCreateAPIView): queryset = Book.objects.all() def get_serializer_class(self): if self.request.method == 'POST': return BookCreateUpdateSerializer return BookListSerializer
Ou encore, filtrer les résultats pour n'afficher que les emprunts de l'utilisateur concerné :
class UserLoansView(generics.ListAPIView): serializer_class = LoanSerializer def get_queryset(self): # Retourne uniquement les emprunts de l'utilisateur connecté return Loan.objects.filter(user=self.request.user)
Avant de passer à la suite, amusez-vous à implémenter les vues pour Book et Loan. Sachant que pour Book, nous avons deux sérialiseurs. Petite astuce : il n'est plus nécessaire de passer la requête en contexte au sérialiseur des livres, les vues génériques le font automatiquement.
class BookListCreateView(generics.ListCreateAPIView): queryset = Book.objects.all() serializer_class = BookListSerializer def get_serializer_class(self): if self.request.method == 'POST': return BookCreateUpdateSerializer return BookListSerializer
class BookDetailView(generics.RetrieveUpdateDestroyAPIView): queryset = Book.objects.all() serializer_class = BookCreateUpdateSerializer
class LoanListCreateView(generics.ListCreateAPIView): queryset = Loan.objects.all() serializer_class = LoanSerializer
class LoanDetailView(generics.RetrieveUpdateDestroyAPIView): queryset = Loan.objects.all() serializer_class = LoanSerializer
Pensez aux URLs :
# Exemple pour les livres path('books/', BookListCreateView.as_view(), name='book-list-create'), path('book/<int:pk>/', BookDetailView.as_view(), name='book-detail'),
Les ModelViewSets
Et si je vous disais que l'on pouvait regrouper les opérations CRUD (Create, Read, Update, Delete) dans une seule classe ?
Implémentons des ModelViewSet pour nos modèles :
from rest_framework import viewsets class AuthorViewSet(viewsets.ModelViewSet): queryset = Author.objects.all() serializer_class = AuthorSerializer
class BookViewSet(viewsets.ModelViewSet): queryset = Book.objects.all() def get_serializer_class(self): if self.action in ["create", "update", "partial_update"]: return BookCreateUpdateSerializer return BookListSerializer
class LoanViewSet(viewsets.ModelViewSet): queryset = Loan.objects.all() serializer_class = LoanSerializer
Maintenant, nous allons utiliser un routeur dans urls.py :
# urls.py # Nettoyons notre fichier pour n'utiliser que les routers from django.contrib import admin from django.urls import path, include from rest_framework import routers from library.views import AuthorViewSet, BookViewSet, LoanViewSet router = routers.DefaultRouter() router.register(r'authors', AuthorViewSet) router.register(r'books', BookViewSet) router.register(r'loans', LoanViewSet) urlpatterns = [ path('admin/', admin.site.urls), path('api/', include(router.urls)), ]
Si je me rends à l'adresse http://127.0.0.1:8000/api/ :
Liste des endpoints API
Par exemple, pour l'auteur, vous pouvez vous rendre sur http://127.0.0.1:8000/api/authors/ pour la liste et la création d'une instance, ou sur http://127.0.0.1:8000/api/authors/2/ pour le détail.
Il est également possible d'ajouter des points d'entrée personnalisés dans les ModelViewSet à l'aide du décorateur @action :
from rest_framework.decorators import action class BookViewSet(viewsets.ModelViewSet): queryset = Book.objects.all() def get_serializer_class(self): if self.action in ["create", "update", "partial_update"]: return BookCreateUpdateSerializer return BookListSerializer @action(detail=True, methods=["get"]) def loans(self, request, pk=None): # detail=True car on accède à un objet spécifique # methods pour les méthodes autorisées """ Récupérer tous les emprunts associés à un livre spécifique. """ book = self.get_object() loans = Loan.objects.filter(book=book) serializer = LoanSerializer(loans, many=True) return Response(serializer.data)
Maintenant, puisque j'ai un emprunt, si je me rends sur http://127.0.0.1:8000/api/books/1/loans/ :
action personnalisée
Filtrage et recherche
Pour filtrer, rechercher et trier des résultats, je vous conseille d'installer django-filter.
Configuration
Commençons par installer la bibliothèque :
pip install django-filter
Ensuite, nous allons dans le fichier settings.py pour ajouter django_filters aux applications ainsi que la constante REST_FRAMEWORK :
# settings.py # Code existant ... INSTALLED_APPS = [ 'django.contrib.admin', 'django.contrib.auth', 'django.contrib.contenttypes', 'django.contrib.sessions', 'django.contrib.messages', 'django.contrib.staticfiles', # DRF et application library 'rest_framework', 'django_filters', 'library', ] # Code existant .... REST_FRAMEWORK = { 'DEFAULT_FILTER_BACKENDS': [ # Filtrage via django-filters 'django_filters.rest_framework.DjangoFilterBackend', # Recherche textuelle 'rest_framework.filters.SearchFilter', # Tri des champs 'rest_framework.filters.OrderingFilter', ], }
-
DjangoFilterBackend: permet de filtrer par valeurs exactes (ex :?author=1) -
SearchFilter: recherche textuelle partielle dans plusieurs champs (ex :?search=django) -
OrderingFilter: permet de trier les résultats (ex :?ordering=titleou?ordering=-idpour l'ordre décroissant)
Il s'agit ici d'une configuration globale, mais il est également possible de redéfinir la configuration pour chaque vue.
Utilisation dans les vues
Si nous reprenons la vue BookViewSet :
class BookViewSet(viewsets.ModelViewSet): queryset = Book.objects.all() filterset_fields = ['author'] # Filtrage par auteur search_fields = ['title', 'author__name'] # Recherche textuelle sur le titre et le nom de l'auteur ordering_fields = ['title', 'author__name'] # Tri par titre et nom d'auteur def get_serializer_class(self): if self.action in ["create", "update", "partial_update"]: return BookCreateUpdateSerializer return BookListSerializer @action(detail=True, methods=["get"]) def loans(self, request, pk=None): """ Récupérer tous les emprunts associés à un livre spécifique. """ book = self.get_object() loans = Loan.objects.filter(book=book) serializer = LoanSerializer(loans, many=True) return Response(serializer.data)
Utilisation de l'attribut filterset_fields : http://127.0.0.1:8000/api/books/?author=2.
Exemple filterset_fields
Un exemple pour l'attribut search_fields : http://127.0.0.1:8000/api/books/?search=django.
Exemple de recherche avec le titre d'un livre
Pour ordering_fields : http://127.0.0.1:8000/api/books/?ordering=title.
Ordonné par titre
Et un exemple avec le nom de l'auteur : http://127.0.0.1:8000/api/books/?ordering=author__name.
Ordonné via le nom de l'auteur
Utilisation avancée des filtres
Pour permettre les comparaisons de dates, les plages de valeurs ou les filtres booléens, il faut créer des FilterSet personnalisés. En effet, nos précédents filtres ne permettaient que des correspondances exactes.
Commençons par créer le fichier filters.py et notre premier filtre personnalisé :
# filters.py from django_filters import rest_framework as django_filters from .models import Loan class LoanFilterSet(django_filters.FilterSet): borrow_date_after = django_filters.DateFilter( field_name="borrow_date", lookup_expr="gte" ) borrow_date_before = django_filters.DateFilter( field_name="borrow_date", lookup_expr="lte" ) is_active = django_filters.BooleanFilter( field_name="return_date", lookup_expr="isnull" ) class Meta: model = Loan fields = ["user", "book"]
Les attributs déclarés dans la classe n'ont pas besoin d'être listés dans fields. En spécifiant user et book, on génère automatiquement deux filtres supplémentaires basés sur les champs du modèle Loan.
On utilisera donc fields pour la génération de filtres simples.
Retournons dans views.py pour utiliser notre FilterSet :
# views.py # Autres imports from .filters import LoanFilterSet class LoanViewSet(viewsets.ModelViewSet): queryset = Loan.objects.all() serializer_class = LoanSerializer filterset_class = LoanFilterSet
Si je me rends sur l'URL http://127.0.0.1:8000/api/loans/ :
Utilisation de la vue LoanViewSet
On applique le filtre http://127.0.0.1:8000/api/loans/?is_active=true :
Filtre is_active
J'ai ajouté un emprunt dans ma base de données avec une date d'emprunt au 1er janvier 2024. Par conséquent, si je me rends sur l'URL http://127.0.0.1:8000/api/loans/ :
Liste des emprunts
Si j'applique le filtre borrow_date_after du FilterSet http://127.0.0.1:8000/api/loans/?borrow_date_after=2025-01-01 :
Filtre borrow_date_after
On pourrait également combiner plusieurs filtres pour récupérer les emprunts actifs après une certaine date : http://127.0.0.1:8000/api/loans/?user=1&is_active=true&borrow_date_after=2025-01-01
Vous pouvez également utiliser les filtres depuis l'interface de l'API :
Bouton pour les filtres
Utilisation des filtres depuis l'interface
Pagination
La pagination est cruciale car, sans elle, votre API risque de retourner des milliers d'objets en une seule requête. Elle permet de diviser les résultats en pages plus petites, réduisant ainsi le temps de chargement.
Configuration globale
Dans le fichier settings.py, ajoutons la pagination à la constante REST_FRAMEWORK :
REST_FRAMEWORK = { 'DEFAULT_FILTER_BACKENDS': [ # Filtrage via django-filters 'django_filters.rest_framework.DjangoFilterBackend', # Recherche textuelle 'rest_framework.filters.SearchFilter', # Tri des champs 'rest_framework.filters.OrderingFilter', ], "DEFAULT_PAGINATION_CLASS": "rest_framework.pagination.PageNumberPagination", "PAGE_SIZE": 10, }
Toutes les listes sont désormais paginées à raison de 10 éléments par page. Après avoir créé plusieurs auteurs dans l'interface d'administration, l'affichage de la liste des auteurs confirme la mise en place de la pagination :
Pagination
Personnalisation de la pagination
Il est également possible de créer une classe pour personnaliser la pagination. Commençons par créer un fichier pagination.py dans l'application library :
# pagination.py from rest_framework.pagination import PageNumberPagination class CustomPagination(PageNumberPagination): page_size = 10 # Nombre d'éléments par défaut page_size_query_param = 'page_size' # Permet au client de changer la taille max_page_size = 100 # Limite maximale
Utilisons maintenant cette classe avec la vue AuthorViewSet :
# views.py # autres imports from .pagination import CustomPagination class AuthorViewSet(viewsets.ModelViewSet): queryset = Author.objects.all() serializer_class = AuthorSerializer pagination_class = CustomPagination
En accédant à l'URL http://127.0.0.1:8000/api/authors/?page=2&page_size=20, on affiche la page 2 avec 20 éléments :
Page 2, on affiche 20 éléments
À noter
À noter qu'il est possible d'utiliser d'autres styles de pagination. Pour en savoir plus, je vous invite à consulter cette section de la documentation : https://www.django-rest-framework.org/api-guide/pagination/
Authentification
Par défaut, l'API est accessible à tout le monde. L'authentification permet d'identifier l'auteur d'une requête. DRF propose plusieurs systèmes d'authentification :
-
SessionAuthentication : réutilise le système de session de Django en l'adaptant aux vues DRF. Très pratique pour ajouter des endpoints à une application Django MVT existante utilisant HTMX
-
TokenAuthentication : système de tokens simples intégré à DRF (basique, sans date d'expiration)
-
JWT (JSON Web Token) : le standard moderne incluant l'expiration et le renouvellement (refresh)
Dans cet article, nous allons nous concentrer sur JWT (Simple JWT) pour les raisons suivantes :
-
C'est le standard pour les API modernes
-
Il gère l'expiration automatiquement
-
Il ne nécessite pas de stockage en base de données
-
Il est indispensable pour les applications mobiles et les frontends tels que Vue.js ou React
Authentification par session
L'authentification par session utilise les cookies Django traditionnels. Elle est utile si votre API est consommée par un frontend Django classique (templates) hébergé sur le même domaine.
Il convient d'utiliser la configuration suivante dans le fichier settings.py :
# settings.py REST_FRAMEWORK = { 'DEFAULT_AUTHENTICATION_CLASSES': [ 'rest_framework.authentication.SessionAuthentication', ], }
Le cas d'usage typique consiste à appeler une vue DRF au sein d'un projet Django MVT afin de la combiner avec HTMX. Toutefois, pour des endpoints HTMX simples, une JsonResponse Django native suffit. SessionAuthentication devient réellement pertinente si vous souhaitez bénéficier de l'écosystème DRF ou si vous prévoyez d'ajouter d'autres méthodes d'authentification ultérieurement.
Authentification par jeton (Token Authentication)
DRF propose un système de tokens intégré. Chaque utilisateur reçoit un token unique stocké en base de données :
# settings.py INSTALLED_APPS = [ # ... 'rest_framework.authtoken', ] REST_FRAMEWORK = { 'DEFAULT_AUTHENTICATION_CLASSES': [ 'rest_framework.authentication.TokenAuthentication', ], }
Il faut ensuite exécuter les migrations : python manage.py migrate.
Il suffit ensuite d'ajouter cette vue au fichier d'URLs : from rest_framework.authtoken.views import obtain_auth_token.
Je passe assez rapidement sur ce système d'authentification afin de me concentrer sur JWT par la suite, mais voici à quoi les tokens ressemblent :
Création d'un token directement depuis l'administration
Les tokens sont sans date d'expiration et sont stockés en base de données sans mécanisme de renouvellement, ce qui n'est pas idéal pour un environnement de production.
Tokens JWT avec Simple JWT
Standard moderne pour les API, le token JWT offre les avantages suivants :
-
tokens non stockés en base de données : évite d'exécuter une requête supplémentaire pour valider l'accès
-
Expiration automatique des tokens : renforce la sécurité des accès
-
Tokens de renouvellement (refresh tokens) : permet d'obtenir de nouveaux tokens d'accès de manière transparente, sans solliciter à nouveau le mot de passe de l'utilisateur
-
Compatibilité universelle : fonctionne avec React, Vue.js, Angular, Flutter, etc.
La bibliothèque Simple JWT est devenue la référence pour implémenter JWT avec Django REST Framework :
Installation et configuration
Commençons par installer la bibliothèque dans l'environnement virtuel : pip install djangorestframework-simplejwt.
Ensuite, modifions le fichier settings.py :
# settings.py INSTALLED_APPS = [ # autres applications # ... 'rest_framework_simplejwt', ] # Et la constante REST_FRAMEWORK # Je met ma configuration globale arrivée à ce stade 'DEFAULT_AUTHENTICATION_CLASSES': [ 'rest_framework_simplejwt.authentication.JWTAuthentication', ], 'DEFAULT_FILTER_BACKENDS': [ # Filtrage via django-filters 'django_filters.rest_framework.DjangoFilterBackend', # Recherche textuelle 'rest_framework.filters.SearchFilter', # Tri des champs 'rest_framework.filters.OrderingFilter', ], "DEFAULT_PAGINATION_CLASS": "rest_framework.pagination.PageNumberPagination", "PAGE_SIZE": 10, }
Maintenant, nous pouvons ajouter les URL :
# urls.py # Autres imports... from rest_framework_simplejwt.views import ( TokenObtainPairView, TokenRefreshView, ) urlpatterns = [ # Autres URLs path('api/token/', TokenObtainPairView.as_view(), name='token_obtain_pair'), path('api/token/refresh/', TokenRefreshView.as_view(), name='token_refresh'), ]
-
TokenObtainPairViewpermet d'obtenir les tokens d'accès et de renouvellement en renseignant le nom d'utilisateur et le mot de passe ; elle fait désormais office de vue de connexion -
TokenRefreshViewpermet de renouveller le token d'accès afin de maintenir la session active -
TokenVerifyViewpermet de s'assurer de la validité d'un token. L'usage de cette vue est plus rare, car elle reste optionnelle
Obtention des tokens
Rendez-vous sur l'URL http://127.0.0.1:8000/api/token/ :
Vue d'obtention des tokens
Réponse avec les tokens
Le client peut ensuite stocker ces deux tokens :
-
Token d'accès (access token) : à envoyer avec chaque requête à l'API
-
Token de renouvellement (refresh token) : à utiliser pour obtenir un nouveau token d'accès
À noter
Pour tester un endpoint protégé, en n'autorisant que les utilisateurs authentifiés par exemple, nous pouvons modifier légèrement BookViewSet
Nous reviendrons plus en détail sur le concept de permissions ultérieurement dans cet article.
# views.py # Autres imports from rest_framework.permissions import IsAuthenticated class BookViewSet(viewsets.ModelViewSet): # Autres attributs existants permission_classes = [IsAuthenticated] # Méthodes existantes
Imaginons que votre vue soit protégée par une permission (notion que nous aborderons à la fin de ce guide) ; pour y accéder, il sera nécessaire de renseigner le token d'accès :
# Requête avec curl vers la vue pour récupérer les livres (j'ai protégé la vue pour cet exemple) # Nous reviendrons plus tard sur les permissions # Ici je fais une requête via mon terminal avec curl curl http://127.0.0.1:8000/api/books/ -H "Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...suite_du_token"
À noter
L'interface web navigable de DRF ne prend pas en charge l'authentification JWT par défaut. Pour tester les endpoints nécessitant un token JWT, il est nécessaire d'utiliser des outils tels que cURL (la méthode la plus directe, illustrée dans l'exemple précédent)
Pour ma part, j'aurais tendance à utiliser la bibliothèque drf-spectacular, qui permet de générer une interface OpenAPI.
Accès à la vue avec un Token JWT
Tentative d'accès à la vue sans Token
Le client peut facilement obtenir un nouveau token d'accès via la vue TokenRefreshView si celui-ci n'est plus valide.
Permissions
Nous venons d'aborder l'authentification, qui permet d'identifier l'utilisateur. Nous allons maintenant étudier les permissions, qui définissent les actions autorisées pour chaque profil.
Configuration globale
Il est possible de définir une permission par défaut dans le fichier settings.py :
REST_FRAMEWORK = { 'DEFAULT_PERMISSION_CLASSES': [ 'rest_framework.permissions.IsAuthenticated', ], }
Tentez d'accéder à n'importe quelle vue : l'accès vous sera refusé. Par exemple, si vous vous rendez sur la racine de l'API http://127.0.0.1:8000/api/ :
Tentative d'accès au router
À noter
Pour la suite de ce guide, je vais retirer la permission par défaut afin de définir les permissions directement au niveau des vues.
Permissions intégrées
Voici les permissions intégrées qu'il est possible d'importer pour vos vues (nous considérerons qu'elles sont déjà importées dans les exemples suivants) :
from rest_framework.permissions import AllowAny, IsAuthenticated, \ IsAdminUser, IsAuthenticatedOrReadOnly
AllowAny
Sans configuration explicite des permissions dans settings.py, DRF utilise par défaut AllowAny. Tout le monde peut y accéder.
Si une configuration par défaut IsAuthenticated était définie (comme au début de la section sur les permissions), il serait possible de spécifier AllowAny au niveau de la vue pour en autoriser l'accès public :
# views.py class AuthorViewSet(viewsets.ModelViewSet): queryset = Author.objects.all() serializer_class = AuthorSerializer pagination_class = CustomPagination permission_classes = [AllowAny]
IsAuthenticated
Seuls les utilisateurs authentifiés peuvent y accéder :
# views.py class AuthorViewSet(viewsets.ModelViewSet): queryset = Author.objects.all() serializer_class = AuthorSerializer pagination_class = CustomPagination permission_classes = [IsAuthenticated]
IsAdminUser
Seuls les administrateurs peuvent accéder à la vue :
# views.py class AuthorViewSet(viewsets.ModelViewSet): queryset = Author.objects.all() serializer_class = AuthorSerializer pagination_class = CustomPagination permission_classes = [IsAdminUser]
IsAuthenticatedOrReadOnly
L'accès aux vues est public en lecture seule, mais la création, la modification ou la suppression de données nécessitent d'être authentifié :
# views.py class AuthorViewSet(viewsets.ModelViewSet): queryset = Author.objects.all() serializer_class = AuthorSerializer pagination_class = CustomPagination permission_classes = [IsAuthenticatedOrReadOnly]
Permissions personnalisées
Bien que les permissions standards de DRF couvrent les principaux cas d'usage, il est possible de créer ses propres permissions en héritant de la classe BasePermission
Les permissions personnalisées permettent d'encapsuler des règles métier spécifiques, évitant ainsi de multiplier les conditions au sein de chaque vue.
Les méthodes de permission
Une permission personnalisée peut implémenter deux méthodes appelées à des moments distincts. Créons une permission permettant de vérifier l'accès général à la vue afin d'autoriser les utilisateurs authentifiés. Ensuite, pour les opérations sur un objet spécifique, nous restreindrons les modifications au seul propriétaire de l'objet.
Créons un fichier permissions.py :
# permissions.py from rest_framework.permissions import BasePermission, SAFE_METHODS class IsOwnerOrReadOnly(BasePermission): def has_permission(self, request, view): return request.user.is_authenticated def has_object_permission(self, request, view, obj): if request.method in SAFE_METHODS: return True return obj.user == request.user
has_permission contrôle l'accès général à la vue en n'autorisant que les utilisateurs authentifiés. Ensuite, has_object_permission permet de restreindre les actions sur un objet spécifique : elle autorise la lecture pour tous les utilisateurs authentifiés (via SAFE_METHODS), mais limite la modification au seul propriétaire
Un utilisateur connecté peut consulter l'ensemble des emprunts, mais ne peut modifier ou supprimer que ses propres emprunts.
À noter
SAFE_METHODS est une constante (voir le code source) regroupant les méthodes HTTP en lecture seule ('GET', 'HEAD', 'OPTIONS'). On peut les mettre en opposition aux méthodes unsafe qui modifient les données : POST, PUT, PATCH, DELETE
Attention
has_object_permission n'est appelée que pour les vues de détail. Nous verrons ultérieurement comment filtrer une liste en fonction de l'utilisateur.
Ajoutons cette permission à notre vue :
# views.py # importer la permissions class LoanViewSet(viewsets.ModelViewSet): queryset = Loan.objects.all() serializer_class = LoanSerializer filterset_class = LoanFilterSet permission_classes = [IsOwnerOrReadOnly]
Si vous tentez de supprimer un emprunt dont vous n'êtes pas le propriétaire :
Impossible de supprimer l'emprunt
Un message d'erreur relatif à la permission s'affiche alors.
Si vous tentez de supprimer votre propre emprunt :
Emprunt supprimé
Aucun message d'erreur ne s'affiche, l'emprunt a été supprimé avec succès
Attention
Deux utilisateurs disposant chacun d'emprunts avaient été créés au préalable dans la base de données. Pour une application en production, nous conviendrons que permettre à un utilisateur de supprimer un emprunt est une pratique risquée (voire imprudente), mais nous nous situons ici dans le cadre d'une démonstration technique 😊.
Filtrage des listes
Il est possible de surcharger la méthode get_queryset de ModelViewSet :
# views.py # imports class LoanViewSet(viewsets.ModelViewSet): queryset = Loan.objects.all() serializer_class = LoanSerializer filterset_class = LoanFilterSet permission_classes = [IsOwnerOrReadOnly] def get_queryset(self): user = self.request.user if user.is_staff: return Loan.objects.all() return Loan.objects.filter(user=user)
Un administrateur peut accéder à l'intégralité des emprunts, tandis qu'un utilisateur standard ne visualise que sa propre liste. Ce filtrage s'applique en complément de la permission IsOwnerOrReadOnly.
Un emprunt a été préalablement créé en base de données pour l'utilisateur gabigab118, qui ne possède pas le statut d'administrateur :
Liste des emprunts
Et lorsque vous tentez de récupérer la liste des emprunts avec cet utilisateur :
Emprunt de gabigab118
Seul son emprunt apparaît alors dans les résultats.
Voyez ainsi comment combiner efficacement get_queryset et les permissions 😊.
Permissions par action
Toutefois, les possibilités ne s'arrêtent pas là : il est possible de définir des permissions distinctes en fonction de l'action effectuée.
class BookViewSet(viewsets.ModelViewSet): queryset = Book.objects.all() def get_permissions(self): if self.action in ['list', 'retrieve']: # Lecture publique permission_classes = [AllowAny] elif self.action in ['create', 'update', 'partial_update']: # Modification nécessite authentification permission_classes = [IsAuthenticated] else: # Suppression réservée aux admins permission_classes = [IsAdminUser] return [permission() for permission in permission_classes]
👉 Vous pouvez voir la liste des actions depuis cette page.
Permission sur une action personnalisée
Nous avons vu précédemment qu'il était possible d'implémenter des actions personnalisées au sein de nos ModelViewSet :
class BookViewSet(viewsets.ModelViewSet): queryset = Book.objects.all() permission_classes = [IsAuthenticatedOrReadOnly] @action(detail=False, methods=['get'], permission_classes=[AllowAny]) def available(self, request): # Cette action est publique même si le ViewSet nécessite l'authentification borrowed_books = Loan.objects.filter( return_date__isnull=True ).values_list('book_id', flat=True) # values_list renvoie une liste des IDs des livres empruntés available_books = Book.objects.exclude(id__in=borrowed_books) # Exclut les livres empruntés actuellement serializer = self.get_serializer(available_books, many=True) return Response(serializer.data)
Pour cette action, nous définissons une permission publique, tandis que les endpoints générés par le ModelViewSet conservent la permission IsAuthenticatedOrReadOnly.
Nous arrivons au terme de ce guide, qui vous offre désormais une base solide pour débuter avec DRF
Avant de nous quitter, je souhaite vous partager deux conseils essentiels :
-
Optimiser les performances (une problématique propre à Django plutôt qu'à DRF)
-
Documenter votre API
Optimisation des performances
Revenons sur le modèle Book et le sérialiseur BookListSerializer :
class Book(models.Model): title = models.CharField(max_length=255) author = models.ForeignKey(Author, related_name='books', on_delete=models.CASCADE) def __str__(self): return self.title
class BookListSerializer(serializers.ModelSerializer): author = AuthorSerializer(read_only=True) is_borrowed_by_me = serializers.SerializerMethodField() class Meta: model = Book fields = ['id', 'title', 'author', 'is_borrowed_by_me'] def get_is_borrowed_by_me(self, obj): request = self.context.get("request") if request and request.user.is_authenticated: return Loan.objects.filter(book=obj, user=request.user, return_date__isnull=True).exists() return False
Lorsque vous retournerez la liste des livres dans vos vues, par exemple :
# views.py # Reprenons un exemple basique class BookViewSet(viewsets.ModelViewSet): queryset = Book.objects.all() serializer_class = BookListSerializer
Dans le modèle Book, l'auteur est une clé étrangère. Django va donc exécuter une requête pour récupérer les livres, puis une requête supplémentaire par livre pour récupérer l'auteur. Ce mécanisme s'avère extrêmement coûteux en termes de performances.
Pour les relations many-to-many ou les clés étrangères inverses (reverse ForeignKey), on utilisera prefetch_related. On peut voir dans le modèle Book une relation inverse avec le champ author : related_name='books'.
Ajoutons le champ books, créé par la relation inverse, dans le sérialiseur :
class AuthorSerializer(serializers.ModelSerializer): class Meta: model = Author fields = ['id', 'name' ,'books']
On peut maintenant utiliser prefetch_related dans le AuthorViewSet :
class AuthorViewSet(viewsets.ModelViewSet): queryset = Author.objects.prefetch_related('books').all() serializer_class = AuthorSerializer pagination_class = CustomPagination permission_classes = [IsAuthenticatedOrReadOnly]
Maintenant, au lieu d'avoir une requête pour les auteurs et une pour chaque livre, on aura une requête pour TOUS les livres.
Documentation automatique de l'API
Pour les consommateurs de votre API, il est important de leur fournir une documentation. Nous utiliserons drf-spectacular.
Installation et configuration
Installons la bibliothèque via pip :
pip install drf-spectacular
Ajoutons la bibliothèque dans les applications installées et les configurations nécessaires dans settings.py :
# settings.py INSTALLED_APPS = [ # ... 'drf_spectacular', ] REST_FRAMEWORK = { # ... vos autres configs 'DEFAULT_SCHEMA_CLASS': 'drf_spectacular.openapi.AutoSchema', } SPECTACULAR_SETTINGS = { 'TITLE': 'Library API', 'DESCRIPTION': 'API de gestion de bibliothèque avec auteurs, livres et emprunts', 'VERSION': '1.0.0', 'SERVE_INCLUDE_SCHEMA': False, }
DEFAULT_SCHEMA_CLASS permet à DRF d'utiliser la bibliothèque comme générateur de schéma. Ensuite, SPECTACULAR_SETTINGS permet de configurer les métadonnées de votre API.
Le paramètre SERVE_INCLUDE_SCHEMA contrôle si Swagger doit inclure le schéma directement dans la page (True) ou le charger via l’URL /api/schema/ (False).
Dans notre configuration, nous mettons False car nous exposons déjà l’endpoint /api/schema/ (voir la suite).
Il faut maintenant aller modifier le fichier urls.py :
# urls.py # Autres imports from drf_spectacular.views import ( SpectacularAPIView, SpectacularSwaggerView, SpectacularRedocView, ) urlpatterns = [ # Autres URLs # Schéma DRF Spectacular # Permet de télécharger le schéma OpenAPI en YAML path('api/schema/', SpectacularAPIView.as_view(), name='schema'), # Documentation interactive Swagger UI path('api/schema/swagger-ui/', SpectacularSwaggerView.as_view(url_name='schema'), name='swagger-ui'), # Documentation ReDoc (alternative) path('api/schema/redoc/', SpectacularRedocView.as_view(url_name='schema'), name='redoc'), ] ]
Accéder à la documentation
-
api/schema/: Génère et retourne le schéma OpenAPI complet de votre API. C'est la "source de vérité" qui décrit tous vos endpoints, modèles, paramètres, etc. Ce schéma est ensuite utilisé par les deux interfaces de documentation ci-dessous -
api/schema/swagger-ui/: Interface Swagger UI, une interface web interactive moderne et populaire. Elle permet de visualiser tous vos endpoints organisés, de voir les modèles de données, et surtout de tester directement les requêtes depuis le navigateur en remplissant des formulaires -
api/schema/redoc/: Interface ReDoc, une alternative à Swagger UI sans les fonctionnalités de test interactif de Swagger
Visualisation de Swagger
On peut maintenant voir les ModelViewSet accessibles via le routeur /api/ :
Aperçu des ModelViewSet dans le swagger
Aperçu des ModelViewSet dans le swagger (suite)
Et comme j'ai laissé les vues génériques, vous pouvez voir qu'elles sont aussi documentées :
Vues génériques
Maintenant, regardons la méthode GET pour le BookViewSet. On peut voir le contenu des réponses envoyées, la pagination, l'ordering...
GET pour le BookViewSet
Et comme nous avions spécifié un sérialiseur différent pour la création et la modification, vous pouvez voir la structure du sérialiseur dans la documentation de la méthode POST :
POST pour le BookViewSet
On peut aussi directement tester l'API dans Swagger. Prenons l'exemple du token d'authentification pour voir comment il est possible de s'authentifier depuis Swagger :
Token dans le swagger
Quand on clique sur Try it out, on peut utiliser ses identifiants
Le bouton pour ensuite aller entrer son token d'access
Entrer son token d'accès
À noter
Ajoutez des docstrings dans vos ModelViewSet et vos actions personnalisées, elles apparaîtront automatiquement dans la documentation !
Ce qui est documenté automatiquement
Il sera par exemple capable de documenter les ModelViewSet, les vues génériques, les filtres et les sérialiseurs, mais spectacular ne peut pas "deviner" le contenu dynamique.
@extend_schema_field
Il faut explicitement typer les méthodes utilisées par SerializerMethodField. Car sans cela, Swagger ne sera pas correctement documenté :
On s'attend à un booléen, mais ici c'est "string" qui est affiché.
On va résoudre ce problème 😎 :
# serializers.py # imports existant from drf_spectacular.utils import extend_schema_field class BookListSerializer(serializers.ModelSerializer): author = AuthorSerializer(read_only=True) is_borrowed_by_me = serializers.SerializerMethodField() class Meta: model = Book fields = ['id', 'title', 'author', 'is_borrowed_by_me'] @extend_schema_field(serializers.BooleanField) def get_is_borrowed_by_me(self, obj): request = self.context.get("request") if request and request.user.is_authenticated: return Loan.objects.filter(book=obj, user=request.user, return_date__isnull=True).exists() return False
Le bon type est renvoyé.
@extend_schema
Si votre action loans dans BookViewSet retourne des emprunts (Loan) mais que le ModelViewSet est configuré pour des livres (Book), la documentation sera fausse. Il faut utiliser @extend_schema pour corriger le type de réponse.
Mauvaise documentation par défaut.
# views.py from drf_spectacular.utils import extend_schema class BookViewSet(viewsets.ModelViewSet): queryset = Book.objects.all() filterset_fields = ['author'] # Filtrage par auteur search_fields = ['title', 'author__name'] # Recherche textuelle sur le titre et le nom de l'auteur ordering_fields = ['title', 'author__name'] # Tri par titre et nom d'auteur def get_serializer_class(self): if self.action in ["create", "update", "partial_update"]: return BookCreateUpdateSerializer return BookListSerializer @extend_schema( responses=LoanSerializer(many=True), summary="Historique des emprunts d'un livre", description="Récupère tous les emprunts associés à un livre spécifique.", filters=False, ) @action(detail=True, methods=["get"]) def loans(self, request, pk=None): """ Récupérer tous les emprunts associés à un livre spécifique. """ book = self.get_object() loans = Loan.objects.filter(book=book) serializer = LoanSerializer(loans, many=True) return Response(serializer.data)
On utilise le décorateur @extend_schema pour documenter l'action personnalisée avec le bon sérialiseur, et on désactive les filtres de la vue car on affiche une instance spécifique via son ID.
Documentation de l'action avec le décorateur
Si vous en êtes arrivés là... bravo ! 🍾
Nous avons parcouru un sacré bout de chemin : de la création d'un modèle Django aux différentes fonctionnalités de DRF, pour terminer avec drf-spectacular.
À vos claviers !