Makina Blog
Django Rest Framework : fonctionnement des routeurs (partie 3)
Chose promise, chose due : après avoir mis en avant les bienfaits des Viewset, on va plonger dans le côté obscur de la force de DRF : les routeurs.
Dans la continuité du précédent article sur les Viewset, nous allons maintenant analyser le fonctionnement des routeurs. Les routeurs ont la particularité de générer automatiquement les motifs d'urls des Viewset. Pour y parvenir, on enregistre d'abord des Viewset avec leurs préfixes de chemins. Puis pour chacunes des actions des Viewset (méthodes d'instance), le routeur déduira le motif d'url associé grâce à des routes. Ces routes sont des namedtuples qui définissent les motifs d'urls, leurs noms, et parfois même un mapping entre des actions standard et des méthodes HTTP.
Exemple d'utilisation d'un routeur
Avant de s'aventurer plus loin, on va se rafraîchir un peu la mémoire avec un cas concret. Admettons que l'on ai le Viewset suivant :
from django.db import models
from rest_framework import serializers
from rest_framework.mixins import (
ListModelMixin, RetrieveModelMixin, UpdateModelMixin,
)
from rest_framework.routers import DefaultRouter
from rest_framework.viewsets import GenericViewSet
class Book(models.Model):
pass
class BookSerializer(serializers.ModelSerializer):
class Meta:
model = Book
class BookViewset(ListModelMixin, RetrieveModelMixin, UpdateModelMixin,
GenericViewSet):
queryset = Book.objects.all()
serializer_class = BookSerializer
On va d'abord créer une instance de classe de type routeur :
router = DefaultRouter()
Jusque là, rien d'incroyable. À cette instance, on enregistre le Viewset avec le préfix de chemin books
:
router.register(r'books', BookViewset)
Pour ensuite générer une liste de motif d'urls en appellant la propriété urls
de l'instance :
urlpatterns = router.urls
Voilà, la variable urlpatterns
contient les motifs d'urls :
[
<RegexURLPattern api-root ^$>,
<RegexURLPattern api-root ^\.(?P<format>[a-z0-9]+)/?$>,
<RegexURLPattern book-list ^books/$>,
<RegexURLPattern book-list ^books\.(?P<format>[a-z0-9]+)/?$>,
<RegexURLPattern book-detail ^books/(?P<pk>[^/.]+)/$>,
<RegexURLPattern book-detail ^books/(?P<pk>[^/.]+)\.(?P<format>[a-z0-9]+)/?$>
]
Les mêmes que ceux qu'on aurait fait à la main, mais avec les bonnes pratiques ! Faut bien reconnaitre que c'est cadeau.
Bon, l'introduction est terminée, on peut désormais entrer dans le vif du sujet.
Comment ça marche
Parce que vous savez que rien n'est magique en informatique, on va tenter de percer les mystères des routeurs. Pour cela, reprenons l'exemple précédent. Quand on a généré les motifs d'urls en appelant la propriété urls du routeur, c'est en réalité la méthode get_urls qui a été appelé une première fois pour mettre le résultat en cache. Donc si vous voulez l'appeler 40 fois pour le plaisir, pas de soucis vous pouvez. Dans cette méthode, pour chacun des Viewset enregistrés, le routeur va :
- détecter les routes avec la méthode get_routes. Comme on reviendra en profondeur sur ce point plus tard, on ne va pas rentrer dans les détails. Retenez seulement que le routeur s'appuie sur l'attribut routes de la classe
SimpleRouter
. Cet attribut est une liste denamedtuple
qui contiennent des urls, leurs noms et parfois un mapping entre le nom de méthodes de vues et des méthodes HTTP. - résoudre ces routes pour transformer les jetons comme les identifiants (
lookup
) et ainsi obtenir le pattern d'url final (regex). - créer une instance de motif d'url (
RegexURLPattern
) pour chacune de ces routes avec la regex, le nom et la fonction génératrice de vues du Viewset. Cette fonction est obtenue suite à l'appel de la méthodeas_view()
d'une nouvelle instance du Viewset. Cette méthode reçoit la liste des actions détectées. C'est ainsi que la fonction de vue connaît le couple méthodes d'instances / méthodes HTTP.
Concrètement, à chaque appel HTTP d'un de ces points d'entrée, le dispatcher de Django appelle la fonction génératrice pour le chemin demandé. Rien de nouveau, c'est le comportement classique de Django. Mais cette fonction génératrice va créer à la volée les méthodes d'instances de vues avec les noms des méthodes HTTP supportées. Puis elle finit par appeler la méthode dispatch()
qui exécute la méthode d'instance (action).
Voilà vous savez toute la vérité…vous n'êtes pas trop triste ?
Les types de routes
Vous l'avez peut-être remarqué dans l'attribut routes
. Il existe en effet par défaut deux (types de) routes :
- detail: les urls contiennent l'identifiant de la ressource (
lookup
) - list: les urls ne contiennent pas d'identifiant de la ressource
Typiquement, si le préfix est books
, vous aurez :
- detail:
/books/<pk>/
- list:
/books/
C'est ensuite la méthode HTTP qui déterminera l'action à exécuter pour un même chemin.
Ces deux types de routes couvrent la plupart des besoins et leurs usages sont assez ancrés dans DRF (décorateurs, liens d'API racine, etc). Néanmoins, on verra dans un prochain article que vous pouvez surcharger cette partie.
Les classes de routeurs
Il existe différentes classes de routeurs. La classe de base est SimpleRouter et la plus fréquemment (aveuglément) utilisée est la classe DefaultRouter, surcharge à la précédente. Elle apporte en plus :
- les formats de suffixes
- une vue racine auto-générée par le routeur qui retourne une liste d'url de tous les Viewset. Chaque url est la route list d'un Viewset. C'est principalement (uniquement) utile avec la browsable API. Le but étant de représenter l'arborescence de l'API.
Les puristes vous diront qu'il faut utiliser la classe SimpleRouter
si vous n'avez pas besoin de tout ça. Les pragmatiques vous diront d'utiliser la classe DefaultRouter
juste au cas où. À vous de voir.
Les routes standard
Pour répondre aux besoins classiques des API REST, DRF propose des routes standard. Ces routes définissent des motifs d'url et des méthodes HTTP pour des actions prédéfinies. Autrement dit, ces actions sont des méthodes d'instance dont le nom est réservé. Les routes standard sont déclarées en tant que namedtuples Route
de l'attribut routes de la classe SimpleRouter
:
# List route.
Route(
url=r'^{prefix}{trailing_slash}$',
mapping={
'get': 'list',
'post': 'create'
},
name='{basename}-list',
initkwargs={'suffix': 'List'}
)
# Detail route.
Route(
url=r'^{prefix}/{lookup}{trailing_slash}$',
mapping={
'get': 'retrieve',
'put': 'update',
'patch': 'partial_update',
'delete': 'destroy'
},
name='{basename}-detail',
initkwargs={'suffix': 'Instance'}
)
En regardant l'attribut mapping
de ces namedtuples Route
, on distingue les actions standard et leurs méthodes HTTP suivantes :
Actions | Méthode HTTP |
list create retrieve update partial_update destory | GET POST GET PUT PATCH DELETE |
On remarque aussi la présence de deux routes. Ces deux routes sont en fait les deux types de routes respectives list et detail. Ci-dessous les actions standard regroupées par types de routes :
List | Detail |
---|---|
list | retrieve |
create | update |
partial_update | |
destroy |
Eh oui ! Ne soyez pas surpris ! L'action create
est une route de type list même si vous ne manipulez qu'une seule ressource car il n'y a pas (besoin) d'identifiant de ressource dans l'url POST /<PREFIX>/
.
Pendant la détection des routes, ces routes standard sont simplement ajoutées dans la liste des routes possibles. Par la suite, le routeur vérifie si les actions sont implémentées pour construire leurs motifs d'url.
Dans l'exemple précédent, le Viewset :
class BookViewset(ListModelMixin, RetrieveModelMixin, UpdateModelMixin,
GenericViewSet):
queryset = Book.objects.all()
serializer_class = BookSerializer
implémente par le biais des mixins les méthodes (actions) :
list
(ListModelMixin)retrieve
(RetrieveModelMixin)update
etpartial_update
(UpdateModelMixin)
Donc quand on demande au routeur de générer les motifs d'urls :
router = DefaultRouter()
router.register(r'books', BookViewset)
urlpatterns = router.urls
on obtient :
GET /books/
(list)GET /books/<pk>/
(retrieve)PUT /books/<pk>/
(update)PATCH /books/<pk>/
(partial_update)
Astuce : pour lister les routes disponibles, vous pouvez en phase de développement installer l'application Django Extension et exécuter la commande show_urls
. Les urls sont listées, mais aussi leur emplacement et surtout leur nom. Par contre, vous n'avez pas les méthodes HTTP (normal, ce n'est pas un outil de DRF). Attention, démonstration :
/ rest_framework.routers.APIRoot api-root
/.<format>/ rest_framework.routers.APIRoot api-root
/books.<format>/ books.urls.BookViewset book-list
/books/ books.urls.BookViewset book-list
/books/<pk>.<format>/ books.urls.BookViewset book-detail
/books/<pk>/ books.urls.BookViewset book-detail
Les routes dynamiques
En plus des actions standard, on peut définir ses propres actions. Pour que ces actions soient routées, on utilise les routes dynamiques. Elles sont aussi de type list ou detail. Pour définir des actions dynamiques, il existe les décorateurs list_route et detail_route. Ces décorateurs acceptent des paramètres pour :
- définir les méthodes HTTP acceptées (oui, plusieurs sont possibles)
- surcharger les attributs de la classe Viewset (tous sans exception, alors pas de pitié !)
- modifier l'url et le nom du motif d'url avec le paramètre url_path. Par défaut, c'est le nom de la méthode décorée qui est utilisé pour construire ces éléments.
Voici l'exemple d'un Viewset qui a décidé un bon matin d'avoir deux actions dynamiques :
from django.db import models
from rest_framework import serializers
from rest_framework.decorators import list_route, detail_route
from rest_framework.permissions import AllowAny, IsAuthenticated
from rest_framework.routers import DefaultRouter
from rest_framework.viewsets import GenericViewSet
class Book(models.Model):
pass
class BookSerializer(serializers.ModelSerializer):
class Meta:
model = Book
class BookViewset(GenericViewSet):
queryset = Book.objects.all()
serializer_class = Book
permission_classes = (IsAuthenticated,)
@detail_route(methods=['get'])
def my_custom_detail_action(self, request, *args, **kwargs):
pass # Do some stuff
@list_route(methods=['post'], permission_classes=(AllowAny,),
url_path='a-better-sexy-name')
def my_custom_list_action(self, request, *args, **kwargs):
pass # Do some stuff
router = DefaultRouter()
router.register(r'books', BookViewset)
urlpatterns = router.urls
Pour chacune de ces actions, on retrouve les caractéristiques des deux namedtuples DynamicListRoute
et DynamicDetailRoute
que l'on retrouve eux aussi dans l'attribut routes
de la classe SimpleRouter
:
DynamicListRoute(
url=r'^{prefix}/{methodname}{trailing_slash}$',
name='{basename}-{methodnamehyphen}',
initkwargs={}
)
DynamicDetailRoute(
url=r'^{prefix}/{lookup}/{methodname}{trailing_slash}$',
name='{basename}-{methodnamehyphen}',
initkwargs={}
)
La détection des actions dynamiques dans la méthode get_routes()
nécessite un peu plus d'efforts au routeur. Quand on utilise un des deux décorateurs, ce dernier va ajouter des attributs aux méthodes décorées :
bind_to_methods
: c'est le paramètremethods
du décorateur : c'est la liste des méthodes HTTP acceptées (get
par défaut). À noter qu'il est inutile d'hurler pour fournir ce paramètre, il sera forcé en minuscules.detail
: c'est un flag qui indique si la route est de type detail ou list. Encore plus incroyable, ce flag est mis tout seul en fonction du décorateurdetail_route
oulist_route
appellé ! Ils ont pensé à tout !kwargs
: celui-là par contre est moins évident. "Bah, c'est unkwargs
fourre-tout quoi" ! Eh bien non, c'est même tout à fait normal. Il s'agit de tous les attributs supplémentaires passés au décorateur qui devront surcharger ceux du Viewset. Bon ok, à part deux/trois exceptions sinon c'est pas drôle. En interne, il sera combiné au dictionnaireinitkwargs
. Puis il sera fourni à la fonction génératriceas_view()
du Viewset. Enfin, cette fonction génératrice pourra créer des instances de classe de vues avec ces paramètres qui écraseront les attributs de classes héritées. Ça y est, le mystère duinitkwargs
est levé.
Quand le routeur inspecte les méthodes implémentées du Viewset, il cherche ces attributs pour déterminer si ce sont des routes dynamiques. Si c'est le cas, ces attributs lui permettront de construire un nouveau namedtuple Route
avec les attributs adéquats. C'est ainsi que la route dynamique devient finalement une splendide route classique. La nature est bien faite.
Revenons à notre exemple et voyons ce que l'on obtient une fois que le routeur a effectué ce travail de résolution pour une route dynamique de type detail (en list c'est pareil mais il n'y a pas de lookup). Accrochez-vous, on y va en détail.
L'attribut url=r'^{prefix}/{lookup}/{methodname}{trailing_slash}$'
une fois résolu se décompose en :
{prefix}
=books
, via l'attributprefix
de la méthode register du routeur (soit icirouter.register(r'books', BookViewset)
)
{lookup}
=(?P<pk>[^/.]+)
, je vous épargne les détails mais sachez que vous pouvez modifier ce pattern en jouant avec les attributs de la class Viewset:lookup_prefix
lookup_url_kwarg
lookup_value
{methodname}
= le nom de la méthode d'action (sans modification quelconque). Ce nom de méthode peut être surchargé avec le paramètre url_path des décorateurs. Ce que l'on fait dans cet exemple pour avoir un motif d'url définitivement plus beau gosse.
{trailing_slash}
=/
. Vous pouvez changer vos habitudes de Djangonaute et désactiver le trailing slash à la création de l'instance du routeur. Exemple :DefaultRouter(trailing_slash=False)
Ce qui donne respectivement les motifs d'urls suivants :
GET /books/(?P<pk>[^/.]+)/my_custom_detail_action/
POST /books/a-better-sexy-name/
.
Pour l'attribut name='{basename}-{methodnamehyphen}'
:
{basename}
=book
. Il s'agit en fait du nom de la classe modèleBook
en minuscule via l'attributqueryset
de la classe Viewset. Si cet attributqueryset
n'existe pas, alors il doit être fourni à la fonction register du routeur avec le paramètrebase_name
. Concrètement, vous devriez faire :router.register(r'books', BookViewset, base_name='book')
. Sinon c'est coups de têtes/balayettes ! Bah oui, DRF fait pas encore le café ;-)
{methodnamehyphen}
= nom de la méthode, en remplaçant les underscores par des tirets. Idem, ce nom de méthode peut être surchargé avec le paramètreurl_path
Ce qui donne respectivement les noms de motifs d'urls suivants:
book-my-custom-detail-action
book-a-better-sexy-name
Bref, tout ça pour vous dire que le premier point d'entrée (my_custom_detail_action
) défini une action de plus de type route en GET (avec identifiant). Il hérite du comportement par défaut défini par les attributs de classe du Viewset et du routeur.
Le second point d'entrée (my_custom_list_action
) défini une autre action de type list en POST (pas d'identifiant). Néanmoins, celui-ci est accessible par tous.
Pour finir, dans un autre article, on vous vend les mérites des Viewsets contre les API Views. C'est en grande partie vrai grâce aux routes dynamiques. Au lieu de définir de nouvelles vues d'API View parce qu'elles n'existent pas en tant que route standard dans un Viewset, vous pouvez les déclarer comme méthodes du Viewset grâce aux routes dynamiques. Ainsi, vous bénéficiez de tous les avantages des Viewset. Qu'est-ce qu'on dit ? Merci les routes dynamiques !
À venir
Ce ne fut pas sans douleur que vous êtes devenu un maître routeur aguérri. Rassurez-vous, on a exploré un des aspects les plus complexes de DRF. Les routeurs n'ayant désormais plus de secret pour vous, on verra dans un prochain article comment les intégrer avec Django.
Formations associées
Formations Django
Formation Django initiation
À distance (FOAD) Du 4 au 8 novembre 2024
Voir la formationFormations Django
Formation Django REST Framework
À distance (FOAD) Du 9 au 13 juin 2025
Voir la formationFormations Django
Formation Django avancé
À distance (FOAD) Du 9 au 13 décembre 2024
Voir la formationActualités en lien
Django Rest Framework : intégration des routeurs (partie 4)
Comprendre comment les routeurs fonctionnent est une chose. Savoir s'en servir correctement, c'est mieux ! On va voir que ça n'est pas si simple et que c'est probablement une des parties de DRF la plus étonnante.
Django Rest Framework : les Viewset (partie 2)
Dans un précédent article, nous avions évoqué les bonnes pratiques à appliquer avec les serializers et les exceptions. Dans cet article, nous allons nous intéresser plus particulièrement aux Viewset.
Django Rest Framework : les Serializer et les exceptions (partie 1)
Django Rest Framework est une extension à Django pour développer rapidement des API REST robustes au goût du jour. Reprenant la philosophie Django, la prise en main est rapide et efficace. Néanmoins, certaines notions nécessitent une attention plus particulière que nous allons mettre en avant dans cette suite d'articles.