Django et API REST

nora.nckm.eu

Illustration

Mise en place d'une architecture avec un backend Django et une API REST. Les sources sont disponibles sur sourcehut.

Configuration

La prise en charge des API (Application Programming Interface) REST (Representational State Transfer) dans Django est assurée par le module djangorestframework.

Configuration du module dans settings.xml :

INSTALLED_APPS = [
    ...
    'rest_framework',
    'app',
]

REST_FRAMEWORK = {
    'DEFAULT_PAGINATION_CLASS': 'rest_framework.pagination.PageNumberPagination',
    'PAGE_SIZE': 10
}

Le framework REST est configuré avec l'option de pagination des résultats.

L'API sera accessible à l'adresse http://localhost:8000/rest/ avec un outil comme curl ou httpie:

http http://127.0.0.1:8000/posts.json
http http://127.0.0.1:8000/posts/
http http://127.0.0.1:8000/posts/2/
http POST http://127.0.0.1:8000/posts/ title="My Post"
http -a admin:password --form JSON http://127.0.0.1:8000/posts/ title="My Post"

Sérialiseurs

Les sérialiseurs permettent de convertir des objets (instances de modèles) en types de données natifs (rendus en JSON/XML) et inversement. Le code ressemble à celui des formulaires Django. Une classe Meta définie le modèle et les champs à sérialiser. Des champs supplémentaires sont définis ou redéfinis dans les attributs.

Création des sérialiseurs dans app/serializers.py :

from rest_framework import serializers
from app.models import Profile, Tag, Post

class ProfileSerializer(serializers.ModelSerializer):
    posts = serializers.HyperlinkedRelatedField(many=True, view_name='post-detail', read_only=True)

    class Meta:
        model = Profile
        fields = ('id', 'user', 'website', 'bio', 'posts')

class TagSerializer(serializers.ModelSerializer):
    class Meta:
        model = Tag
        fields = ('id', 'name')

class PostSerializer(serializers.HyperlinkedModelSerializer):
    author = serializers.ReadOnlyField(source='author.user.email')
    content = serializers.CharField(default='Lorem ipsum', help_text='Contenu de l’article', source='body')
    tags = TagSerializer(read_only=True, many=True)
    search_url = serializers.SerializerMethodField('get_search_url')

    class Meta:
        model = Post
        fields = ('url', 'id', 'title', 'subtitle', 'content', 'author', 'publish_date', 'published', 'tags', 'search_url')

    def get_search_url(self, obj):
        return f"https://duckduckgo.com/?q={obj.title}"

Permissions

Les droits d'accès sont gérés avec les permissions.

Définition des permissions dans app/permissions.py :

from rest_framework import permissions

class IsOwnerOrReadOnly(permissions.BasePermission):
    def has_object_permission(self, request, view, obj):
        if request.method in permissions.SAFE_METHODS:
            return True

        return obj.author.user == request.user

Vues fondées sur les fonctions

Les vues fondées sur les fonctions utilisent le décorateur @api_view. Il faut ensuite utiliser les sérialiseurs pour retourner une réponse en fonction de la méthode GET, POST, PUT, DELETE indiquée dans la requête.

Définition des routes dans urls.py :

from app import views

urlpatterns = [
    path('rest/api-auth/', include('rest_framework.urls')),
    path('rest/root/', views.api_root),
    path('rest/posts/', views.PostsFunctionView, name='post-list'),
    path('rest/posts/<int:pk>', views.PostFunctionView, name='post-detail'),
]

La route api-auth active l'authentification et root active la page racine listant les autres entrées de l'API.

Création des vues dans app/views.py :

from rest_framework import status
from rest_framework.decorators import api_view
from rest_framework.response import Response
from rest_framework.reverse import reverse
from app.models import Post
from app.serializers import PostSerializer

@api_view(['GET'])
def api_root(request, format=None):
    return Response({
        'profiles': reverse('profile-list', request=request, format=format),
        'posts': reverse('post-list', request=request, format=format),
    })

@api_view(['GET', 'POST'])
def PostsFunctionView(request, format=None):
    if request.method == 'GET':
        posts = Post.objects.all()
        serializer = PostSerializer(posts, many=True)
        return Response(serializer.data)

    elif request.method == 'POST':
        serializer = PostSerializer(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)

@api_view(['GET', 'PUT', 'DELETE'])
def PostFunctionView(request, pk, format=None):
    try:
        post = Post.objects.get(pk=pk)
    except Post.DoesNotExist:
        return Response(status=status.HTTP_404_NOT_FOUND)

    if request.method == 'GET':
        serializer = PostSerializer(post)
        return Response(serializer.data)

    elif request.method == 'PUT':
        serializer = PostSerializer(post, 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':
        post.delete()
        return Response(status=status.HTTP_204_NO_CONTENT)

Vues fondées sur les classes

Les vues fondées sur les classes (Class-Based Views) adoptent une approche orientée objet. Un ensemble de classes génériques qui peuvent être étendues et mixées pour construire des vues plus complexes. Une description détaillée est disponible sur le site Classy Django REST Framework.

Class-based views

Les vues fondées sur les classes héritent de APIView. Il faut ensuite redéfinir les méthodes get, post, put, delete.

Définition des routes dans urls.py :

from app import views

urlpatterns = [
    path('rest/posts/', views.PostsClassView.as_view(), name='post-list'),
    path('rest/posts/<int:pk>', views.PostClassView.as_view(), name='post-detail'),
]

Création des vues dans app/views.py :

from django.http import Http404
from rest_framework import status
from rest_framework.response import Response
from rest_framework.views import APIView
from app.models import Post
from app.serializers import PostSerializer

class PostsClassView(APIView):
    def get(self, request, format=None):
        posts = Post.objects.all()
        serializer = PostSerializer(posts, many=True)
        return Response(serializer.data)

    def post(self, request, format=None):
        serializer = PostSerializer(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)

class PostClassView(APIView):
    def get_object(self, pk):
        try:
            return Post.objects.get(pk=pk)
        except Post.DoesNotExist:
            raise Http404

    def get(self, request, pk, format=None):
        post = self.get_object(pk)
        serializer = PostSerializer(post)
        return Response(serializer.data)

    def put(self, request, pk, format=None):
        post = self.get_object(pk)
        serializer = PostSerializer(post, 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, format=None):
        post = self.get_object(pk)
        post.delete()
        return Response(status=status.HTTP_204_NO_CONTENT)

Generic class-based views

Les vues fondées sur les classes de type générique héritent de GenericAPIView et plus spécifiquement des classes ListAPIView, CreateAPIView, RetrieveAPIView, UpdateAPIView, DestroyAPIView... Il faut ensuite préciser le queryset et le serializer et éventuellement les permissions.

Définition des routes dans urls.py :

from app import views

urlpatterns = [
    path('rest/posts/', views.PostsView.as_view(), name='post-list'),
    path('rest/posts/<int:pk>', views.PostView.as_view(), name='post-detail'),
]

Création des vues dans app/views.py :

from rest_framework import permissions
from rest_framework.generics import ListCreateAPIView, RetrieveUpdateDestroyAPIView
from app.models import Post
from app.permissions import IsOwnerOrReadOnly
from app.serializers import PostSerializer

class PostsView(ListCreateAPIView):
    permission_classes = [permissions.IsAuthenticatedOrReadOnly, IsOwnerOrReadOnly]

    queryset = Post.objects.all()
    serializer_class = PostSerializer

    def perform_create(self, serializer):
        serializer.save(owner=self.request.user)

class PostView(RetrieveUpdateDestroyAPIView):
    permission_classes = [permissions.IsAuthenticatedOrReadOnly, IsOwnerOrReadOnly]

    queryset = Post.objects.all()
    serializer_class = PostSerializer

Generic class-based viewsets

Les vues fondées sur les classes de type viewsets héritent de GenericViewSet et plus spécifiquement des classes ReadOnlyModelViewSet ou ModelViewSet. Il faut ensuite préciser le queryset et le serializer et éventuellement les permissions. On peut leur associer un routeur afin de gérer les routes dynamiquement. Il s'agit de la forme la plus générique des vues.

Définition des routes dans urls.py :

from rest_framework.routers import DefaultRouter
from app import views

router = DefaultRouter()
router.register(r'profiles', views.ProfileViewSet, basename="profile")
router.register(r'posts', views.PostViewSet, basename="post")

urlpatterns = [
    path('rest/', include(router.urls)),
]

Création des vues dans app/views.py :

from rest_framework import permissions, viewsets
from app.models import Profile, Post
from app.permissions import IsOwnerOrReadOnly
from app.serializers import ProfileSerializer, PostSerializer

class ProfileViewSet(viewsets.ReadOnlyModelViewSet):
    queryset = Profile.objects.all()
    serializer_class = ProfileSerializer

class PostViewSet(viewsets.ModelViewSet):
    queryset = Post.objects.all()
    serializer_class = PostSerializer
    permission_classes = [permissions.IsAuthenticatedOrReadOnly, IsOwnerOrReadOnly]

    @action(methods=['get'], detail=True, renderer_classes=[renderers.StaticHTMLRenderer])
    def highlight(self, request, *args, **kwargs):
        post = self.get_object()
        return Response(post.highlighted)

    def perform_create(self, serializer):
        serializer.save(owner=self.request.user)

Le décorateur @action permet de définir une action personnalisée.

Un commentaire sur un de mes articles ? Commencez une discussion sur ma liste de diffusion en envoyant un email à ~nora/public-inbox@lists.sr.ht [règles]