Makina Blog
Django Rest Framework : personnalisation des routeurs (partie 5)
Dernière ligne droite pour les routeurs : après avoir analysé le fonctionnement et l'intégration, c'est au tour de la personnalisation.
Personnaliser les routes
"Les routes dynamiques, c'est bien mais je n'arrive pas à faire exactement ce que je veux. Pourtant, c'est pas faute d'avoir essayé des trucs tordus comme jouer avec les attributs lookup\*"…
Tiens tiens, vous n'essayez pas de forcer des paramètres supplémentaires dans l'url par hasard ?
"Et sinon, je peux ajouter un chemin en plus avec des paillettes pour faire plus sexy ?"
Heureusement oui. On peut même imaginer déclarer des actions de masses type create/update/delete.
Vous l'avez compris, une des solutions possibles est la personnalisation d'un routeur. Cependant, sachez qu'il est préférable d'avoir des besoins simples. Préparez-vous également à devoir instancier plusieurs classes de routeurs. Autrement dit, on va devoir s'écarter un peu des préconisations…
En règle générale, il suffit de surcharger l'attribut routes
de la classe SimpleRouter
(ou DefaultRouter
si vous tenez à conserver les formats de suffixes et vues d'API racine). Vous pouvez ajouter à cette liste de routes vos propres routes, voir la modifier complètement. Après bien sûr, vous pouvez aussi surcharger n'importe quelles méthodes du routeur.
Trêve de bavardages et au boulot ! Clonez ce dépôt et allez sur la branche routing_custom
. N'oubliez pas de lancer la commande de migration ./manage.py migrate
.
Notre exercice va porter uniquement sur l'application bar
. On va commencer par déclarer un routeur personnalisé dans le fichier routers.py
de l'application bar
. Ce routeur va :
- permettre de faire des opérations en masse de modifications et suppressions
- conserver les actions de détails classiques
- fournir pour tous les chemins de type list un argument numérique et un chemin en plus. Ceci concerne donc aussi les routes dynamiques
Voici une implémentation possible :
from rest_framework.routers import DefaultRouter, DynamicListRoute, Route
class MyCustomRouter(DefaultRouter):
routes = list(DefaultRouter.routes)
routes[0] = Route(
url=r'^{prefix}/a-custom-path/(?P<custom_pk>\d+){trailing_slash}$',
mapping={
'get': 'list',
'post': 'create',
'patch': 'partial_update_multiple',
'delete': 'delete_multiple',
},
name='{basename}-list',
initkwargs={'suffix': 'List'}
)
routes[1] = DynamicListRoute(
url=r'^{prefix}/a-custom-path/(?P<custom_pk>\d+)/{methodname}{trailing_slash}$',
name='{basename}-{methodnamehyphen}',
initkwargs={}
)
Ce que l'on fait ici :
- on créé une nouvelle classe routeur qui hérite de la classe
DefaultRouter
parce qu'on aime la diversité des formats - on effectue une copie de la liste des routes parentes pour modifier les routes de type list standard et dynamique. On conserve donc les autres routes, c'est-à-dire les routes de détail. Par contre, on a plus les anciennes routes de type list. C'est donc un routeur spécifique.
- chacune des routes modifiées a désormais un nouveau chemin
a-custom-path
parce que c'est trop swagg (c'est comme ça qu'on dit ? Punaise ça me fait mal !). - un nouveau paramètre numérique
custom_pk
doit être fourni - les actions standards sont étendues pour associer les méthodes HTTP
PATCH
etDELETE
avec de nouvelles méthodes d'instance nomméespartial_update_multiple
etdelete_multiple
. Trop classe.
Maintenant qu'on a un routeur flambant neuf, il faut modifier le Viewset en conséquence pour implémenter les deux actions de masses et l'action dynamique :
from rest_framework.decorators import list_route
from rest_framework.response import Response
from rest_framework.viewsets import ModelViewSet
from .models import Bar
class BarViewSet(ModelViewSet):
queryset = Bar.objects.all()
def partial_update_multiple(self, request, custom_pk, *args, **kwargs):
return Response(
'Do some bulk update op for the given custom pk : {pk}'.format(pk=custom_pk)
)
def delete_multiple(self, request, custom_pk, *args, **kwargs):
return Response(
'Do some bulk delete op for the given custom pk : {pk}'.format(pk=custom_pk)
)
@list_route(methods=['get'], url_path='so-dope-path')
def a_custom_callback(self, request, custom_pk, *args, **kwargs):
return Response(
'Like American people say : AWESOME! We now have a custom '
'dynamic list route with a custom path and take a custom pk '
'which is : {pk}'.format(pk=custom_pk)
)
Rien de particulier hormis un détail qui vous a peut-être échappé : regardez la classe héritée du Viewset. Comme c'est la classe ModelViewSet
, les actions standard sont déjà implémentées. Donc ça n'est pas visible, mais les routes de détail qu'on avait copiées sans modification dans le routeur sont bien implémentées par le Viewset.
Il ne reste plus qu'à utiliser ce nouveau routeur et enfreindre une de nos propres règles :
from django.conf.urls import include, url
from rest_framework.routers import DefaultRouter
from bar.routers import MyCustomRouter
from bar.viewsets import BarViewSet
from baz.viewsets import BazViewSet
from foo.viewsets import FooViewSet
router = DefaultRouter()
router.register('api/baz', BazViewSet)
router.register('api/foo', FooViewSet)
custom_router = MyCustomRouter()
custom_router.register('api/bar', BarViewSet)
urlpatterns = [
# Some Web views
url(r'^foo/', include('foo.urls', namespace='foo')),
url(r'^bar/', include('bar.urls', namespace='bar')),
url(r'^baz/', include('baz.urls', namespace='baz')),
]
# Append our API views
urlpatterns += router.urls
urlpatterns += custom_router.urls
Voilà, on a deux routeurs, beurk. Mais désormais, les motifs d'urls générés sont :
/ rest_framework.routers.APIRoot api-root
/ rest_framework.routers.APIRoot api-root
/.<format>/ rest_framework.routers.APIRoot api-root
/.<format>/ rest_framework.routers.APIRoot api-root
/api/bar/<pk>.<format>/ bar.viewsets.BarViewSet bar-detail
/api/bar/<pk>/ bar.viewsets.BarViewSet bar-detail
/api/bar/a-custom-path/<custom_pk>.<format>/ bar.viewsets.BarViewSet bar-list
/api/bar/a-custom-path/<custom_pk>/ bar.viewsets.BarViewSet bar-list
/api/bar/a-custom-path/<custom_pk>/so-dope-path.<format>/ bar.viewsets.BarViewSet bar-so-dope-path
/api/bar/a-custom-path/<custom_pk>/so-dope-path/ bar.viewsets.BarViewSet bar-so-dope-path
/api/baz.<format>/ baz.viewsets.BazViewSet baz-list
/api/baz/ baz.viewsets.BazViewSet baz-list
/api/baz/<pk>.<format>/ baz.viewsets.BazViewSet baz-detail
/api/baz/<pk>/ baz.viewsets.BazViewSet baz-detail
/api/foo.<format>/ foo.viewsets.FooViewSet foo-list
/api/foo/ foo.viewsets.FooViewSet foo-list
/api/foo/<pk>.<format>/ foo.viewsets.FooViewSet foo-detail
/api/foo/<pk>/ foo.viewsets.FooViewSet foo-detail
/bar/mywebview/ bar.views.MyWebView bar:mywebview
/baz/mywebview/ baz.views.MyWebView baz:mywebview
/foo/mywebview/ foo.views.MyWebView foo:mywebview
Si vous lancez un serveur avec la commande ./manage.py runserver
, vous pouvez vérifier le bon fonctionnement en exécutant les requêtes suivantes :
curl -X PATCH 127.0.0.1:8000/api/bar/a-custom-path/15/
curl -X DELETE 127.0.0.1:8000/api/bar/a-custom-path/15/
curl -X GET 127.0.0.1:8000/api/bar/a-custom-path/15/so-dope-path/
Remarquez que les actions standard de détails sont bien implémentées et retournent un joli code 404 car aucune ressource avec l'id 1 n'existe :
curl -v -X GET 127.0.0.1:8000/api/bar/1/
curl -v -X PUT 127.0.0.1:8000/api/bar/1/
curl -v -X PATCH 127.0.0.1:8000/api/bar/1/
curl -v -X DELETE 127.0.0.1:8000/api/bar/1/
Éviter les routeurstein
Dans l'exemple précédent, on a adopté une approche minimaliste en surchargeant uniquement l'attribut routes
. Notre fainéantise n'est pas anodine car elle nous permet de rester simple. Et ce qui est simple est facile à maintenir.
Si vous êtes amené à trop surcharger le routeur, il est possible d'obtenir un routeurstein difficile à maintenir (ouais, ça m'est venu comme ça. J'ai osé). Rappellez-vous que les routeurs sont là pour vous faire gagner du temps en produisant très peu de code. Si vous perdez du temps à surcharger (bidouiller) les mécanismes par défaut des routeurs car vous avez des besoins particuliers, c'est probablement que le routeur n'est pas la solution à retenir. N'oubliez pas, vous pouvez tout-à-fait générer manuellement les motifs d'urls d'un Viewset comme le fait un routeur. En reprenant notre projet d'exemple précédent (allez sur la branche no_router
), on pourrait générer les mêmes motifs d'url comme ceci :
from django.conf.urls import include, url
from bar.viewsets import BarViewSet
from baz.viewsets import BazViewSet
from foo.viewsets import FooViewSet
urlpatterns = [
url(r'^api/bar/$', BarViewSet.as_view({
'get': 'list',
'post': 'create',
}), name='bar-list'),
url(r'^api/bar/(?P<pk>[^/.]+)/$', BarViewSet.as_view({
'get': 'retrieve',
'put': 'update',
'patch': 'partial_update',
'delete': 'destroy',
}), name='bar-detail'),
url(r'^api/baz/$', BazViewSet.as_view({
'get': 'list',
'post': 'create',
}), name='baz-list'),
url(r'^api/baz/(?P<pk>[^/.]+)/$', BazViewSet.as_view({
'get': 'retrieve',
'put': 'update',
'patch': 'partial_update',
'delete': 'destroy',
}), name='baz-detail'),
url(r'^api/foo/$', FooViewSet.as_view({
'get': 'list',
'post': 'create',
}), name='foo-list'),
url(r'^api/foo/(?P<pk>[^/.]+)/$', FooViewSet.as_view({
'get': 'retrieve',
'put': 'update',
'patch': 'partial_update',
'delete': 'destroy',
}), name='foo-detail'),
# Some Web views
url(r'^foo/', include('foo.urls', namespace='foo')),
url(r'^bar/', include('bar.urls', namespace='bar')),
url(r'^baz/', include('baz.urls', namespace='baz')),
]
C'est moche hein ? Bon, ceci n'est qu'un exemple pour vous montrer que les routeurs ne sont pas indispensables. Néanmoins, on réalise aussi que finalement les routeurs, c'est de la boulette !
Bonnes pratiques des url REST
Même si on sort du cadre de cet article dédié exclusivement à DRF, j'aimerai terminer sur quelques conseils pour vos urls finales :
- le préfix de chemin, c'est le nom de votre ressource au pluriel. Cependant, il arrive parfois qu'on ne manipule qu'une seule ressource. C'est le cas avec l'utilisateur courant. Par exemple, avoir
/user/
ou/account/
a du sens. Mais pour les autres situations, on manipule en générale plusieurs ressources. Admettons que le modèle soit `Book`, le préfix de chemin est donc/books/
et non/book/
. Sinon, ca veut dire que votre bibliothèque tient sur la commode des W.C. - elles sont courtes. Par conséquent, vous avez de nombreux points d'entrées principales et peu de profondeur relationnelle. Par exemple, ne faites pas
GET /books/authors/<author_pk>/
mais plutôtGET /authors/<pk>/
. Néanmoins, on s'accorde en général la possibilité d'avoir le premier point d'entrée enfant qui est la vue de liste. Par exempleGET /books/authors/
. Pas plus ! Stop ! Fini ! Sinon on risque d'avoir des urlsmega/longue/et/super/galere/a/maintenir
avec un seul point d'entrée principal qui règnera en maître absolu dans votre documentation d'API et qui fera ramer votre Swagger.
Non pas que toutes ces bonnes pratiques soient innées, ce document m'a grandement aidé dans mon apprentissage.
Conclusion
Personnaliser les routeurs est parfois nécessaire pour répondre à des besoins spécifiques. Toutefois, il est préférable que ces besoins ne soient pas trop complexes. Cette personnalisation n'est pas parfaite. Elle peut notamment nous obliger à multiplier le nombre de routeurs. Poussée à l'extrême, elle peut même devenir inmaintenable. Il convient donc d'en limiter son usage. Toutefois, si la personnalisation des routeurs est utilisée convenablement, les routeurs peuvent nous dévoiler tous leurs potentiels.
À venir
Voilà, notre périple s'achève (enfin !) ici pour les routeurs. Le prochain article sera dédié au versioning.
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 : négociation de contenu (partie 6)
Après s'être remis de nos émotions avec les routeurs, on va s'accorder une petite trêve avec un thème qui ne déchaîne pas les passions : la négociation de contenu. Pourtant, c'est un passage obligatoire car c'est un élément essentiel aux API REST pour communiquer avec les clients. Allez, courage !
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 : 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.