BROUILLON, ARTICLE EN COURS DE REDACTION (CORRIGER LES FAUTES ETC...)
J'ai récement publié un article : Créer une API REST en Python. Dans ce guide, nous allons nous intéressé à Django Rest Framework (on l'appelera DRF), largement utilisé pour le développement d'APIs. Et si vous connaissez déjà Django, 80% du travaille 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 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 une interface web
-
DRF fournit des classes pour la 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 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 une API de système de gestion de bibliothèque. 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 Dango 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 de 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 serialiers.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 avec 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 sauvegarde en base si valide 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 avec une vue de détail
Maintenant on va 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 identifé par sa clé primaire. On essaye de le récupérer dans la base de données, en cas d'échec on retourne une erreur 404.
Ensuite, plusieurs actions 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 supprime l'auteur de la base de données et retourne un statut 204
De manière générale, il préférable de spécifier le statut lorsqu'il est différent du 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 ignora 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 ce 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'essaye 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
Quand 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 vu précédemment fonctionnement également avec ModelSerializer.
Nous allons maintenant refactoriser notre serializer :
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 serializers 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 : 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 nous 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 serializer 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 lus temporaire, ou que l'on a besoin du contexte de la requête on préferera un SerializerMethodField.
À vous de jouer
Voici comment récupérer la requête dans les serializers (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
Il faudrait bien penser à passer la requête depuis la vue :
@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, essayer d'implémenter vous-même un SerializerMethodField qui permet de savoir si j'ai emprunté un livre. Sinon, passer directement à la suite, nous allons le faire ensemble.
Commençons par implémenter une méthode au 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 ajouter une vue simple pour afficher les livres, tout en passant la requête au serializer :
# 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 et rapide, 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 le clé primaire de l'auteur. Mais il est possible de le 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 que réellement nous n'avons 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/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 une 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 des 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
À côté de ça on aurait un serializer pour la création/modification :
class BookCreateUpdateSerializer(serializers.ModelSerializer): class Meta: model = Book fields = ['id', 'title', 'author']
Et dans la vue on utiliserait selon les deux serializers en fonction de la méthode utilisé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 il y a pas mal de code répétitif. Nous ne reviendrons pas dessus (n'hésitez pas à revenir sur les @api_view que nous avons codé précédemment).
La classe APIView
Ce n'est que mon avis personnel, mais tant qu'à redéfinir mes méthodes HTTP, autant utiliser l'APIView. Très semblable à ce que l'on a fait différemment 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 pas mal de code répétitif. Passons à la suite.
Les vues génériques
DRF propose des vues qui permettent de réduire le code. Commençons par importer from rest_framework import generics dans views.py.
ListCreateAPIView
On remplace 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 status
De plus, lorsque vous accéder à 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 parler de la vue de détail :
class AuthorDetailView(generics.RetrieveUpdateDestroyAPIView): queryset = Author.objects.all() serializer_class = AuthorSerializer
La classe permet de gérer GET, PUT, PATCH et DELETE sur un objet spécifique.
Je vous invite à regarder 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 serializers 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 des umprunts d'un utilisateur en question :
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 serializers. Petite astuce, plus besoin de passer la reuqête en contexte au serializer les 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 le 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 router 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 pour le détail http://127.0.0.1:8000/api/authors/2/.
Il est aussi possible d'ajouter des endpoints personnalisés dans les ModelViewSet avec le 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, comme j'ai un emprunt, si je me rends sur http://127.0.0.1:8000/api/books/1/loans/ :
action personnalisée
Filtrer et rechercher
Pour filtrer, chercher et trier des résultats, je vous conseille d'installer django-filter.
Configuration
Commençons par installer la librairie :
pip install django-filter
Ensuite nous allons dans le fichier settings.py pour ajouter django_filters dans les applications et 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 exacts (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 ordre décroissant)
Il s'agit ici d'une configuration globale, mais il est aussi 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, des plages de valeur, ou des filtres booléens, il faut créer des FilterSet personnalisés. En effet, nos précédents filtres permettaient des correspondances exacts.
Commençons par créers 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 rajouté un emprunt dans ma base de donnée avec un date d'emprunt au 1er janvier 2024, donc si je vais 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 aussi combiner plusieurs filtres pour récupérer les emprunts actifs après une certaine date : http://127.0.0.1:8000/api/loans//loans/?user=1&is_active=true&borrow_date_after=2025-01-01.
Vous pouvez aussi utiliser les filtres depuis l'interface de l'API :
Bouton pour les filtres
Utilisation des filtres depuis l'interface
Pagination
La pagination est très importante, car sans elle, votre API peut retourner des milliers d'objets en une requête. La pagination permet alors de diviser les résultats en pages plus petites et plus rapides à charger.
Configuration globale
Dans le fichier de 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 paginées avec 10 éléments par page. Je vais aller créer plusieurs auteurs dans l'administration. Et quand j'affiche les auteurs j'ai bien une pagination :
Pagination
Personnaliser la pagination
Il est aussi 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
Maintenant utilisons 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
À savoir qu'il est possible d'utiliser un autre style de pagination. Je vous invite à vous rendre sur cette partie 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 qui fait la requête. DRF propose plusieurs systèmes d'authentification :
-
SessionAuthentification : réutilise le même système de session Django mais l'adapte pour fonctionner les vues DRF. Pratique si on ajoute des endpoints pour HTMX avec une application Django MVT existante
-
TokenAuthentication : Token simple intégré à DRF (basique, sans expiration)
-
JWT (JSON Web Token) : LE standard moderne avec expiration et refresh
Pour cet article, nous allons nous concentrer sur JWT (Simple JWT) car :
-
C'est le standard pour les APIs modernes
-
Gère l'expiration automatiquement
-
Pas de stockage en base
-
Indispensable pour le mobile et des frontend colmme Vue JS ou React
Session Authentication
L'authentification par session utilise les cookies Django traditionnels. C'est utile si votre API est consommée par un frontend Django classique (templates) sur le même domaine.
Il faudrait utiliser cette configuration dans le fichier settings.py :
# settings.py REST_FRAMEWORK = { 'DEFAULT_AUTHENTICATION_CLASSES': [ 'rest_framework.authentication.SessionAuthentication', ], }
Le cas d'usage typique serait d'appeler une vue DRF dans un projet Django MVT pour la combiner à HTMX. Mais pour de simples endpoints HTMX, dans ce cas JsonResponse Django natif suffit. SessionAuthentication devient vraiment utile si vous souhaitez bénéficier de l'écosystème DRF ou si vous prévoyez d'ajouter d'autres méthodes d'authentification plus tard.
Token Authentification DRF
DRF propose un système de tokens intégré. Chaque utilisateur reçoit un token unique stocké en base.
# 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 vite sur ce système d'authentification pour 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 expiration et les tokens sont stockés en base de données sans mécanisme de refresh. Ce qui n'est pas idéal pour un environnement de production.
Tokens JWT avec Simple JWT
Le standard moderne pour les APIs, le token JWT offre :
-
Tokens non stockés en base de données : donc pas besoin d'exécuter une requête pour récupérer le token
-
Expiration automatique des tokens : sécurité renforcée
-
Refresh tokens : les nouveaux tokens sont récupérés de manière transparente sans redemander le mot de passe
-
Compatibilité universelle : les tokens fonctionnent avec React, Vue, Angulat, Flutter...
La librairie Simple JWT est devenue la référence pour implémenter JWT avec Django REST Framework.
Installation et configuration
Commençons par installer la librairie dans l'environnement : pip install djangorestframework-simplejwt. Puis dans 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 urls :
# 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 rafraichissement en renseignant l'username et le mot de passe, c'est maintenant votre vue de login -
TokenRefreshViewpermet de renouveller le token d'accès afin de rester connecté -
TokenVerifyViewpermet de vérifier la validité d'un token. Cette vue est en générale plus rare car c'est optionnel
Obtention des tokens
Rendez-vous sur 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 :
-
Access token à envoyer dans chaque requête API
-
Refesh token à utiliser pour obtenir un nouveau access token
À noter
Pour tester un endpoint protégé, en n'autorisant que les utilisateurs connectés par exemple, on peut légèrement modifier BookViewSet.
Nous reviendrons sur le concept de permissions plus tard 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
Imaginez que votre vue soit protégée par une permission (ce que l'on verra à la fin du guide), pour y accéder il faudra 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 browsable de DRF ne supporte pas l'authentification JWT par défaut. Pour tester les endpoints qui nécessitent un token JWT il faut utiliser des outils comme cURL (le plus simple, ce que je montre dans l'exemple juste au-dessus).
Pour ma part j'aurais tendance à utiliser la librairie DRF-Spectacular qui permet la génération d'une interface OpenAPI.
Accès à la vue avec un Token JWT
Tentative d'accès à la vue sans Token
Le client peut facilement récupérer un nouveau token d'accès si celui si n'est plus valide via la vue TokenRefreshView.
Permissions
Nous venons de voir l'authentification qui permet de savoir qui vous êtes. Nous allons maintenant voir les permissions qui permettent de savoir qui peut faire quoi.
Configuration globale
Dans le fichier settings.py il est possible de définir une permission par défaut :
REST_FRAMEWORK = { 'DEFAULT_PERMISSION_CLASSES': [ 'rest_framework.permissions.IsAuthenticated', ], }
Essayez d'accéder à n'importe quelle vue, l'accès y sera refusé. Par exemple, si je me rends sur le router 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 pour utiliser 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 (on considerera qu'ils sont importés pour les exemples suivants) :
from rest_framework.permissions import AllowAny, IsAuthenticated, \ IsAdminUser, IsAuthenticatedOrReadOnly
AllowAny
Sans configuration explicite de permissions dans settings.py, DRF utilise par défaut AllowAny. Tout le monde peut accéder.
Si on avait une configuration par défaut IsAuthenticated (comme au début de la section sur les permissions), on pourrait spécifier au niveau de la vue AllowAny pour permettre à tout le monde d'y accéder :
# views.py class AuthorViewSet(viewsets.ModelViewSet): queryset = Author.objects.all() serializer_class = AuthorSerializer pagination_class = CustomPagination permission_classes = [AllowAny]
IsAuthenticated
Seuls les utilisateurs peuvent accéder :
# views.py class AuthorViewSet(viewsets.ModelViewSet): queryset = Author.objects.all() serializer_class = AuthorSerializer pagination_class = CustomPagination permission_classes = [IsAuthenticated]
IsAdminUser
Seul 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 publique, mais les créations/modifications/suppressions 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 BasePermission.
Les permissions personnalisées permettent d'encapsuler des règles métiers spécifiques, plutôt que d'utiliser des conditions dans chaque vue.
Les méthodes de permission
Une permission personnalisée peut implémenter deux méthodes qui sont appelées à différents moments. Créons une permission qui permet de de vérifier l'accès général à la vue : autoriser les utilisateurs authentifiés. Ensuite, pour les opérations sur un objet spécifique, nous allons restreindre les modifications uniquement au 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 autorisant que les utilisateurs authentifiés. Puishas_object_permission permet de contrôler les actions sur un objet spécifique. Elle authorise la lecture pour tous les utilisateurs authentifiés (via SAFE_METHODS), mais restreint la modification uniquement au propriétaire.
Un utilisateur connecté peut consulter les emprunts, mais ne peut modifier ou supprimer que des propres emprunts.
À noter
SAFE_METHODS est une constante (voir le code source) qui contient les méthodes HTTP en lecture seule ('GET', 'HEAD', 'OPTIONS'). On peut les mettre en oppostion 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 plus tard comment il est possible de filtrer une liste selon l'utilisateur.
Ajoutons cette permissions à 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 j'essaye de supprimer un emprunt qui n'est pas à moi :
Impossible de supprimer l'emprunt
Nous avons un message d'erreur en rapport avec la permission.
Si j'essaye de supprimer mon emprunt :
Emprunt supprimé
Pas de message d'erreur, l'emprunt a été supprimé.
Attention
J'avais au préalable deux utilisateurs dans la base avec chacun des emprunts. Dans une application en production on est d'accord que de donner la permission à un utilisateur de supprimer un emprunt c'est dangereux (je dirais même inconscient), mais nous sommes ici dans la 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 a accès à tous les emprunts, et un utilisateur n'a accès qu'à la liste de ses emprunts. Sachant que l'on combine ce filtre avec la permission IsOwnerOrReadOnly.
Dans ma base j'ai créé un emprunt pour l'utilisateur qui n'est pas staff gabigab118 :
Liste des emprunts
Et quand j'essaye de récupérer la liste des emprunts avec cet utilisateur :
Emprunt de gabigab118
Je n'ai bien que son emprunt.
Voyez comment il est possible de combiner get_queryset et les permissions 😊.
Permission par actions
Mais ce n'est pas terminé... Il faut savoir qu'il est possible d'avoir des permissions différentes selon l'action :
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 custom
Nous avons vu précédemment qu'il était possible d'implémenter des actions customisées dans 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)
Dans cette action, nous avons une permission publique pour l'action, alors que l'accès aux endpoints générés par le ModelViewSet ont la permission IsAuthenticatedOrReadOnly.
**Nous arrivons à la fin de ce guide qui vous donne déjà une bonne base pour vous lancer avec DRF. **
Mais avant de se quitter je vais vous donner deux conseils :
-
Optimiser les performances (ce n'est pas propore à DRF mais à Django)
-
Documenter son API
Optimisation des performances
Revenons sur le serializer le modèle Book et le serializer 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. Donc Django va devoir faire une requête pour récupérer les livres, puis une requête PAR livre pour récupérer l'auteur. Ce qui est très coûteux en terme de performance.
Pour les relations Many-to-Many ou les 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 serializer :
class AuthorSerializer(serializers.ModelSerializer): class Meta: model = Author fields = ['id', 'name' ,'books']
On peut