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.