Makina Blog

Le blog Makina-corpus

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 et DELETE avec de nouvelles méthodes d'instance nommées partial_update_multiple et delete_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ôt GET /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 exemple GET /books/authors/. Pas plus ! Stop ! Fini ! Sinon on risque d'avoir des urls mega/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

Nantes Du 12 au 14 mars 2024

Voir la formation

Formations Django

Formation Django Rest Framework

À distance (FOAD) Du 10 au 14 juin 2024

Voir la formation

Formations Django

Formation Django avancé

À distance (FOAD) Du 9 au 13 décembre 2024

Voir la formation

Actualités en lien

14/01/2016

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 !

Voir l'article
Image
Django_Rest_Framework_routeurs2
06/01/2016

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.

Voir l'article
Image
Django_Rest_Framework_routeurs3
06/01/2016

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.

Voir l'article

Inscription à la newsletter

Nous vous avons convaincus