Makina Blog

Le blog Makina-corpus

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.

Implémentation des routeurs dans les URLconf

Parlons des choses qui fâchent. Ce qui est le plus surprenant avec les routeurs, c'est la façon de les déclarer dans le fichier racine d'URLconf.

Avec Django, on prend vite l'habitude d'utiliser naturellement le mécanisme d'inclusion d'URLconf. Cela permet de conserver la logique métier des chemins dans les applications respectives et de mettre les préfixes de chemins uniquement dans le projet pour les personnaliser. Chacun son job, keep it stupid !

Avec DRF, il est fréquent de voir cette bonne pratique oubliée et pour cause : tous les routeurs sont déclarés dans le fichier d'URLconf racine. Les applications n'ont pas leur mot à dire sur le chemin souhaité. Tous les imports de Viewset sont faits dans le fichier d'URLconf racine à la bourrin. La séparation n'est plus stricte. Diviser pour mieux régner, c'était avant. C'est triste.

Ci-dessous l'arborescence d'un projet d'exemple dont l'originalité vous émerveille :

example
|
--manage.py
--example
  |
  --settings.py
  --urls.py
  --wsgi.py
|
--foo
  |
  --models.py
  --urls.py
  --views.py
  --viewsets.py
|
--bar
  |
  --models.py
  --urls.py
  --views.py
  --viewsets.py
|
--baz
  |
  --models.py
  --urls.py
  --views.py
  --viewsets.py

Dans ce projet, on a 3 applications : foo, bar et baz. Chacunes de ces applications ont des vues Web et des vues d'API (si, si, ce besoin est fréquent). On a délibérement choisi de regrouper les vues Web et les vues d'API dans la même application (mais dans des modules Python différents), plutôt que de faire une application genre api (ne vous cachez pas, je vous ai grillé !). Pourquoi ? Parce que c'est logique. Après tout, les mêmes ressources (modèles) sont utilisées, les mêmes fonctions helper, etc. Parce que c'est beau aussi. Parce que c'est pour l'exemple.

Voici le fichier d'URLconf racine example.urls, dans lequel tous les routeurs sont déclarés. C'est la méthode officielle recommandée :

from django.conf.urls import include, url

from rest_framework.routers import DefaultRouter

from bar.viewsets import BarViewSet
from baz.viewsets import BazViewSet
from foo.viewsets import FooViewSet


router = DefaultRouter()
router.register('api/bar', BarViewSet)
router.register('api/baz', BazViewSet)
router.register('api/foo', FooViewSet)

urlpatterns = [
    # Some Web views
    url(r'^web/foo/', include('foo.urls', namespace='foo')),
    url(r'^web/bar/', include('bar.urls', namespace='bar')),
    url(r'^web/baz/', include('baz.urls', namespace='baz')),
]
# Append our API views
urlpatterns += router.urls

Bon ok, là il n' y a que 3 applications…Du coup à voir comme ça, c'est plutôt propre !

Maintenant, voyons le contenu des fichiers urls.py des applications (ils sont identiques pour chacunes des applications, ouais je me suis pas foulé) :

from django.conf.urls import url

from . import views

urlpatterns = [
    url(r'^mywebview/$', views.MyWebView.as_view(), name='mywebview'),
]

Rien de plus banal. Clonez le repository git, mettez-vous sur la branche routing_urlconf_root et préparez-vous un virtualenv. Quand vous êtes prêt, exécutez la commande de Django Extension : ./manage.py show_urls.

Quoi, vous avez la flemme ? Bon ok, voici le retour de la commande :

/	rest_framework.routers.APIRoot	api-root	
/.<format>/	rest_framework.routers.APIRoot	api-root	
/api/bar.<format>/	bar.viewsets.BarViewSet	bar-list	
/api/bar/	bar.viewsets.BarViewSet	bar-list	
/api/bar/<pk>.<format>/	bar.viewsets.BarViewSet	bar-detail	
/api/bar/<pk>/	bar.viewsets.BarViewSet	bar-detail	
/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	
/web/bar/mywebview/	bar.views.MyWebView	bar:mywebview	
/web/baz/mywebview/	baz.views.MyWebView	baz:mywebview	
/web/foo/mywebview/	foo.views.MyWebView	foo:mywebview

Jusque ici, tout va bien. Mais il y a un détail qui ne vous a pas échappé : "Les vues d'API n'ont pas de namespace ? Car c'est cool les namespaces ! Moi j'aime les namespaces !".

Soyez sans crainte, DRF pense à tout. Eh oui, vous pouvez le faire en utilisant la fonction include. Eh oui, vous ne pourrez appliquer qu'un seul namespace… Et paf ! Si vous voulez un namespace par application, vous devrez faire du sale. En fait, vous devez avoir autant de routeurs que de namespace. Essayez par vous-même, vous verrez bien. Pour ma part, je m'avoue vaincu, j'ai pas réussi à faire mieux:

from django.conf.urls import include, url

from rest_framework.routers import DefaultRouter

from bar.viewsets import BarViewSet
from baz.viewsets import BazViewSet
from foo.viewsets import FooViewSet


router_bar = DefaultRouter()
router_bar.register('bar', BarViewSet)

router_baz = DefaultRouter()
router_baz.register('baz', BazViewSet)

router_foo = DefaultRouter()
router_foo.register('foo', FooViewSet)

urlpatterns = [
    # Some Web views
    url(r'^web/foo/', include('foo.urls', namespace='foo')),
    url(r'^web/bar/', include('bar.urls', namespace='bar')),
    url(r'^web/baz/', include('baz.urls', namespace='baz')),
    # Dirty stuff begins
    url(r'^api/', include(router_bar.urls, namespace='bar')),
    url(r'^api/', include(router_baz.urls, namespace='baz')),
    url(r'^api/', include(router_foo.urls, namespace='foo')),
]

Huuum, ça commence à piquer, non ? Ne partez pas ! Si on regarde de plus près les chemins générés (allez sur la branche routing_urlconf_root_namespace) :

/api/	rest_framework.routers.APIRoot	bar:api-root	
/api/	rest_framework.routers.APIRoot	baz:api-root	
/api/	rest_framework.routers.APIRoot	foo:api-root	
/api/.<format>/	rest_framework.routers.APIRoot	bar:api-root	
/api/.<format>/	rest_framework.routers.APIRoot	baz:api-root	
/api/.<format>/	rest_framework.routers.APIRoot	foo:api-root	
/api/bar.<format>/	bar.viewsets.BarViewSet	bar:bar-list	
/api/bar/	bar.viewsets.BarViewSet	bar:bar-list	
/api/bar/<pk>.<format>/	bar.viewsets.BarViewSet	bar:bar-detail	
/api/bar/<pk>/	bar.viewsets.BarViewSet	bar:bar-detail	
/api/baz.<format>/	baz.viewsets.BazViewSet	baz:baz-list	
/api/baz/	baz.viewsets.BazViewSet	baz:baz-list	
/api/baz/<pk>.<format>/	baz.viewsets.BazViewSet	baz:baz-detail	
/api/baz/<pk>/	baz.viewsets.BazViewSet	baz:baz-detail	
/api/foo.<format>/	foo.viewsets.FooViewSet	foo:foo-list	
/api/foo/	foo.viewsets.FooViewSet	foo:foo-list	
/api/foo/<pk>.<format>/	foo.viewsets.FooViewSet	foo:foo-detail	
/api/foo/<pk>/	foo.viewsets.FooViewSet	foo:foo-detail	
/web/bar/mywebview/	bar.views.MyWebView	bar:mywebview	
/web/baz/mywebview/	baz.views.MyWebView	baz:mywebview	
/web/foo/mywebview/	foo.views.MyWebView	foo:mywebview

Vous remarquerez que les vues d'API ont désormais les namespace des applications, youhou ! Vous remarquerez aussi autre chose : "C'est quoi tous ces /api/ ?". Rappelez-vous, ce sont les fameuses vues d'API racine auto-générées. Cela prend tout son sens dans l'exemple précédent (branche routing_urlconf_root) car on obtient une magnifique liste de liens tip top cool. En revanche dans ce cas, c'est totalement absurde car :

  • on a 3 routeurs
  • les routeurs ont un seul Viewset
  • le préfix de chemin est le même pour tous les routeurs. "Mais au fait en cas de conflit, c'est qui qui gagne ?". Je vous laisse deviner…

On peut toujours se dire que c'est pas grave. "Allez, c'est bon, on a rien vu et on garde ça entre nous. Personne n'a besoin de connaître ce point d'entrée /api/" ;-). On peut aussi se persuader que c'est génial et que si on utilise la classe SimpleRouter à la place de DefaultRouter, problème réglé ! Bye-bye les vues d'API racine ! Bye-bye le support des formats de suffixes aussi… Bref avouons-le, cela démontre clairement que cette implémentation n'est pas bonne du tout.

Alors on va faire une ultime tentative parce que vous avez d'autres choses à faire. Pour cette tentative de la dernière chance, on va tenter d'avoir des namespaces par applications et de déplacer proprement les routeurs dans les fichiers d'URLconf des applications respectives. Oui Mesdames et Messieurs, vous avez bien lu : on va envoyer du rêve !

C'est parti, opération coup de balai dans le fichier racine d'URLconf. Voici un aperçu de son relooking :

from django.conf.urls import include, url

urlpatterns = [
    url(r'^', include('foo.urls', namespace='foo')),
    url(r'^', include('bar.urls', namespace='bar')),
    url(r'^', include('baz.urls', namespace='baz')),
]

"Je me sens plus léger, plus frais. Même si j'ai des chemins vides qui veulent rien dire, je m'en fiche, je me sens nouveau".

Et les fichiers d'URLconf des applications (je vous n'en montre qu'un seul):

from django.conf.urls import url

from rest_framework.routers import DefaultRouter

from .views import MyWebView
from .viewsets import BarViewSet

router = DefaultRouter()
router.register('api/bar', BarViewSet)

urlpatterns = [
    url(r'^web/bar/mywebview/$', MyWebView.as_view(), name='mywebview'),
]
urlpatterns += router.urls

"Aaaaah. Enfin je me sens utile. Vous venez de donner un sens à mon existence ! En plus, c'est moi qui commande et qui décide des url complètes, héhé !"

Positionnez-vous sur la branche routing_urlconf_apps et exécutez la commande ./manage.py show_urls:

/	rest_framework.routers.APIRoot	bar:api-root	
/	rest_framework.routers.APIRoot	baz:api-root	
/	rest_framework.routers.APIRoot	foo:api-root	
/.<format>/	rest_framework.routers.APIRoot	bar:api-root	
/.<format>/	rest_framework.routers.APIRoot	baz:api-root	
/.<format>/	rest_framework.routers.APIRoot	foo:api-root	
/api/bar.<format>/	bar.viewsets.BarViewSet	bar:bar-list	
/api/bar.<format>/	baz.viewsets.BazViewSet	baz:baz-list	
/api/bar/	bar.viewsets.BarViewSet	bar:bar-list	
/api/bar/	baz.viewsets.BazViewSet	baz:baz-list	
/api/bar/<pk>.<format>/	bar.viewsets.BarViewSet	bar:bar-detail	
/api/bar/<pk>.<format>/	baz.viewsets.BazViewSet	baz:baz-detail	
/api/bar/<pk>/	bar.viewsets.BarViewSet	bar:bar-detail	
/api/bar/<pk>/	baz.viewsets.BazViewSet	baz:baz-detail	
/api/foo.<format>/	foo.viewsets.FooViewSet	foo:foo-list	
/api/foo/	foo.viewsets.FooViewSet	foo:foo-list	
/api/foo/<pk>.<format>/	foo.viewsets.FooViewSet	foo:foo-detail	
/api/foo/<pk>/	foo.viewsets.FooViewSet	foo:foo-detail	
/web/bar/mywebview/	bar.views.MyWebView	bar:mywebview	
/web/baz/mywebview/	baz.views.MyWebView	baz:mywebview	
/web/foo/mywebview/	foo.views.MyWebView	foo:mywebview	

Si on ne tient pas compte des vues d'API racine (on a dit que ça restait entre nous hein), on a ce qu'on voulait sur le papier…non ? Bon sérieusement, on voit bien là aussi que c'est pas bon du tout!

"Euuuh, c'est une blague mec, alors c'est tout ? Pas de namespace par application ? On regroupe tout dans le fichier racine d'URLconf et basta ?". Eh bien j'ai le regret de vous annoncer que… oui !! J'ai fait de nombreuses tentatives qui se sont toutes soldées par des échecs car toutes avaient de vilains défauts. Sur un projet, j'ai par exemple tenté de déporter du fichier d'urlConf racine les routeurs dans un fichier d'urlConf inclus pour alléger tout ça mais en vain… c'est franchement pas convainquant ! Bref, continuer à vous les démontrer serait une perte de temps. En revanche, cette fastudieuse démonstration nous a permis d'en tirer les conclusions suivantes :

  1. Un namespace = un fichier d'URLconf inclus = un préfix de chemin. Donc si vous avez un routeur et des urls construites manuellement dans le même fichier d'URLconf inclus parce que c'est plus propre, ils auront le même namespace.
  2. Le préfix d'un routeur ne doit jamais être vide ou identique pour un même niveau d'arborescence. Sérieux ça ressemble à rien.
  3. Le motif d'url (construit manuellement et passer en paramètre à la fonction django.conf.urls.url) ne doit jamais être vide ou identique pour un même niveau d'arborescence. Même tarif : c'est pas lisible.
  4. Un namespace = un routeur. Plusieurs namespaces = plusieurs routeurs. Et plusieurs routeurs dans le même fichier d'URLconf inclus = infraction à la règle 2 ou 3. C'est mal.

Si vous ne respectez pas ces règles, vous allez certainement faire des trucs pas beaux. Mais sinon oui, rassurez-vous, ça marche si c'est la seule chose qui vous préoccupe…

Ensuite, il faut se poser les questions suivantes pour ces vues Web et REST:

  1. Est-ce que je veux le même namespace ?
  2. Est-ce que je veux le même préfix de chemin ?
  3. Est-ce que je veux plusieurs namespaces ?
  4. Est-ce que je veux déclarer les routeurs dans le fichier urls.py des applications car c'est plus propre ?
  5. Est-ce que je veux avoir mal à la tête ?

Pour chacunes de ces questions, si la réponse est négative, tout va bien ! Sinon, vous allez sûrement faire des trucs bizarres dans les exemples ci-dessus…

Avec un peu de recul, on se rend compte finalement que :

  • cela dépend de vos besoins.
  • le design recommandé n'est pas très élégant mais globalement, il répond à tous les besoins si on fait une impasse sur les namespace. Finalement, il faut imaginer que les base_name des noms des motifs remplacent les namespace… Je sais c'est pas top, mais il faut faire avec dans l'immédiat.
  • si on sort des sentiers battus, faut du bon café !

En conclusion, si votre API n'est pas complexe, utilisez un seul routeur et donc un seul namespace (voir aucun). Placez le routeur dans le fichier d'URLconf racine, n'ayez ni honte ni scrupule. C'est la solution officielle préconisée qui s'adapte à de nombreuses situations.

En revanche, si votre API est plus conséquente et que vous trouvez un design du genre : "Bon sang mais oui ! C'est évident !", eh bien je suis preneur !

Utilisation des motifs d'urls nommés

Maintenant que la frustration est passée, on va finir en douceur. On a vu dans un précédent article que les routeurs génèrent aussi des noms aux motifs d'urls. Vous pouvez les utiliser comme ceux des Class Based View car n'oubliez pas, un Viewset est (littéralement) un ensemble de vues. Cependant, il y a une petite subtilité:

from rest_framework.reverse import reverse as reverse_api
reverse_api('book-my-custom-detail-action')

Sans nul doute, votre regard avisé a remarqué que dans cet exemple, on utilise la fonction reverse de DRF plutôt que celle de Django (django.core.urlresolvers.reverse). Cette méthode surcharge celle du core pour :

  • prendre en compte le versioning (on reviendra sur le versioning dans un prochain article)
  • retourner des URL absolues si possible car c'est une bonne pratique. Vos clients d'API suivent l'url sans se poser de questions du type : quel protocole (http/https), quel hostname, quel port, etc. J'en profite donc pour vous faire une piqûre de rappel : renvoyez toujours des URL absolues. Il vous faudra pour cela une instance de la classe HttpRequest sous le coude, utilisée pour construire l'URL absolue à partir de l'url courante. Si vous n'avez pas cet objet request dans le contexte courant (par exemple dans une commande Django), pas de magie. Vous devrez construire l'url absolue vous même. Vous pouvez définir une variable de configuration à concaténer (par exemple BASE_URL = 'http://makina-corpus.com'). Vous pouvez aussi danser le Nae Nae. Pas sûr que ça marche.

Prenez donc l'habitude de toujours utiliser cette fonction plutôt que celle du core quand vous manipulez des vues REST. Même quand vous pensez que ça ne sert à rien.

Enfin remarquez qu'on a pris le soin de renommer la fonction pour éviter tout conflit avec celle du core de Django si celle-ci était également chargée dans notre module Python. Mieux vaut prévenir que guérir comme on dit ! Cela étant, si vous pouvez éviter de regrouper les vues Web et les vues d'API dans le même module Python…ca serait un petit plus !

Conclusion

Sur papier c'est vrai, les routeurs, ça envoi du rêve. Mais dans la vraie vie, on peut se heurter à des petits problèmes de jeunesse comme on a pu le constater. Visiblement, ces désagréments ne sont pas tant liés au routeur en lui-même mais plutôt à son intégration avec le coeur de Django (namespace, inclusion d'URLconf). Ne soyez donc pas surpris si pour parvenir à vos fins, vous devrez parfois vous accorder quelques écarts de bonnes conduites.

Outre ces petits défauts, il n'en reste pas moins que les routeurs offrent de nombreux avantages qu'il serait idiot de ne pas profiter.

À venir

Dans un prochain article, on verra un sujet que vous attendez tous : la personnalisation des routeurs !

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

Image
Django_Rest_Framework_routeurs
06/01/2016

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.

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
27/11/2015

Python : Bien configurer son environnement de développement

Comment utiliser les bonnes pratiques de développement Python.

Voir l'article

Inscription à la newsletter

Nous vous avons convaincus