Créer une API REST avec Django Rest Framework

Découvrez comment créer une API REST avec Django Rest Framework.

Publié le par Gabriel Trouvé (mis à jour le )
190 minutes

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}"
PYTHON

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)
PYTHON

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',
]
PYTHON

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
BASH

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
PYTHON

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)
PYTHON

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=True pour 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éthode create du 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'),
]
PYTHON

Maintenant, allons dans l'administration Django pour créer deux auteurs.

Création des auteurs dans l'administration

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

Récupération des auteurs avec la méthode GET

[
    {
        "id": 1,
        "name": "Patrick"
    },
    {
        "id": 2,
        "name": "Sebastien"
    }
]
JSON

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

Champ pour envoyer des données

Maintenant, nous pouvons créer un nouvel auteur :

Données pour créer le nouvel auteur

Données pour créer le nouvel auteur

Réponse après la création

Réponse après la création

On remarque bien une réponse 201 avec notre auteur.

{
    "id": 3,
    "name": "Gabriel"
}
JSON

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)
PYTHON

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éthode update, 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

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

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

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
PYTHON

Si j'essaie de créer un utilisateur de cette manière via l'URL http://127.0.0.1:8000/authors/ :

    {
        "name": "robert"
    }
JSON

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."
    ]
}
JSON

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']
PYTHON

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']
PYTHON
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
PYTHON

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
PYTHON

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)
PYTHON
# 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'),
]
PYTHON

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 livre dans l'administration Django

Création d'un emprunt 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

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}"
PYTHON

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
PYTHON

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
PYTHON

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)
PYTHON

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
PYTHON

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)
PYTHON

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'),
]
PYTHON

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

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

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']
PYTHON

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']
PYTHON

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
PYTHON
Affichage du nom des auteurs

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
PYTHON

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)
PYTHON
Affichage de l'auteur de manière imbriquée

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']
PYTHON

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)
PYTHON

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)
PYTHON

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'),
]
PYTHON

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)
PYTHON

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'),
]
PYTHON

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
PYTHON

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

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
PYTHON

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

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
PYTHON

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)
PYTHON

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
PYTHON
class BookDetailView(generics.RetrieveUpdateDestroyAPIView):
    queryset = Book.objects.all()
    serializer_class = BookCreateUpdateSerializer
PYTHON
class LoanListCreateView(generics.ListCreateAPIView):
    queryset = Loan.objects.all()
    serializer_class = LoanSerializer
PYTHON
class LoanDetailView(generics.RetrieveUpdateDestroyAPIView):
    queryset = Loan.objects.all()
    serializer_class = LoanSerializer
PYTHON

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'),
PYTHON

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
PYTHON
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
PYTHON
class LoanViewSet(viewsets.ModelViewSet):
    queryset = Loan.objects.all()
    serializer_class = LoanSerializer
PYTHON

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)),
]
PYTHON

Si je me rends à l'adresse http://127.0.0.1:8000/api/ :

Liste des endpoints 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)
PYTHON

Maintenant, puisque j'ai un emprunt, si je me rends sur http://127.0.0.1:8000/api/books/1/loans/ :

action personnalisée

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
BASH

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',
    ],
}
PYTHON
  • 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=title ou ?ordering=-id pour 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)
PYTHON

Utilisation de l'attribut filterset_fields : http://127.0.0.1:8000/api/books/?author=2.

Exemple filterset_fields

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

Exemple de recherche avec le titre d'un livre

Ordonné par titre

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

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"]
PYTHON

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
PYTHON

Si je me rends sur l'URL http://127.0.0.1:8000/api/loans/ :

Utilisation de la vue LoanViewSet

Utilisation de la vue LoanViewSet

Filtre is_active

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

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

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

Bouton pour les filtres

Utilisation des filtres depuis l'interface

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,
}
PYTHON

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

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
PYTHON

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
PYTHON

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

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',
    ],
}
PYTHON

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',
    ],
}
PYTHON

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

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,
}
PYTHON

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'),
]
PYTHON
  • TokenObtainPairView permet 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

  • TokenRefreshView permet de renouveller le token d'accès afin de maintenir la session active

  • TokenVerifyView permet 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

Vue d'obtention des tokens

Réponse avec les 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
PYTHON

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"
BASH

À 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

Accès à la vue avec un Token JWT

Tentative d'accès à la vue sans Token

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',
    ],
}
PYTHON

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

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
PYTHON

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]
PYTHON

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]
PYTHON

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]
PYTHON

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]
PYTHON

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
PYTHON

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]
PYTHON

Si vous tentez de supprimer un emprunt dont vous n'êtes pas le propriétaire :

Impossible de supprimer l'emprunt

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é

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)
PYTHON

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

Liste des emprunts

Et lorsque vous tentez de récupérer la liste des emprunts avec cet utilisateur :

Emprunt de gabigab118

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]
PYTHON

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)
PYTHON

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
PYTHON
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
PYTHON

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
PYTHON

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']
PYTHON

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]
PYTHON

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
SHELL

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,
}
PYTHON

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'),
]
]
PYTHON

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

Aperçu des ModelViewSet dans le swagger (suite)

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

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

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

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

Token dans le swagger

Quand on clique sur Try it out, on peut utiliser ses identifiants

Quand on clique sur Try it out, on peut utiliser ses identifiants

Le bouton pour ensuite aller entrer son token d'access

Le bouton pour ensuite aller entrer son token d'access

Entrer son token d'accès

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

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
PYTHON
Le bon type est renvoyé.

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.

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)
PYTHON

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

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 !

Bravo, tu es prêt à passer à la suite

Rechercher sur le site

Formulaire de contact

Inscris-toi à Docstring

Pour commencer ton apprentissage.

Tu as déjà un compte ? Connecte-toi.