🍪👌👌 L'API la plus parfaite - Partie 2
🚨 Cette édition est relativement longue, votre boîte mail risque de la tronquer. Je vous conseille de cliquer sur le titre ci-dessus pour l’ouvrir dans votre navigateur web. ☝️
Au programme aujourd’hui :
L'API la plus parfaite - Partie 2 par Thomas
L’agence de recrutement pour les leads dévs RoR !
Temps de lecture : 15 minutes
Hello les petits Biscuits !
Bienvenue sur la 17ème édition de Ruby Biscuit.
Vous êtes maintenant 405 abonnés 🥳
Je vous souhaite à toutes et à tous une belle année 2024 et nous fêtons aussi les 1 an de la newsletter ❤️
Merci de nous avoir accompagnés en 2023, nous vous réservons plein de belles surprises pour cette nouvelle année !
Je profite de cette introduction pour vous faire découvrir la chouette newsletter « Quoi de neuf les devs ? ».
C’est une newsletter hebdomadaire gratuite pour les développeuses, développeurs, sysadmin, ops, sre, DevRel, sécu.
Chaque vendredi, Quoi de neuf les devs c’est : des articles, les news des réseaux, des vidéos, des podcasts, des outils, les mises à jour de la semaine, l’invité.e de la semaine, des offres d’emplois, les conférences et événements à venir, la possibilité pour les dévs de faire leur demande de stage, d’alternance, de job, une touche d’humour et une pointe de culture.
On y parle un peu de tout ce qui tourne autour du dév avec une forte appétence, il faut se l’avouer pour le dév web.
Pour voir les derniers numéros et pourquoi pas vous abonner gratuitement c’est ici 👇
Avant de vous laisser entre les mains de Thomas pour l’article du mois, je tenais à vous rappeler que vous êtes tous les bienvenus pour donner votre avis en commentaire et partager vos expériences sur les sujets que nous abordons. Vous pouvez aussi mettre un petit like ❤️ et/ou partager la newsletter à un copain ou une copine ! 😉
Bonne lecture.
L'API la plus parfaite - Partie 2
Bonjour, fier membre de la communauté Ruby Biscuit ! La dernière fois, on avait parlé de faire une API la plus flexible possible côté client, la plus customisable possible et on s'était arrêté après avoir mis en place quelques gems qui nous ont permis d'exposer une API très proche de la norme jsonapi.org.
Aujourd'hui on continue et on finit cette exploration. L'objectif maintenant c'est de rajouter des fonctionnalités qui vont nous permettre d'avoir beaucoup plus de contrôle pour exposer exactement ce qu'on veut, à qui on veut. On verra aussi les actions de create et d'update et on terminera avec des trucs et astuces pour faciliter les tests. C'est parti !
Note : Cet article est la suite de l'édition précédente et reprend le code là où on l'avait laissé. Je vous invite à vous rafraîchir la mémoire au besoin parce que ça va démarrer très vite !
Disclamer : Comme cette édition est très longue, je vous conseille vivement de l’ouvrir dans votre navigateur. Dans la version navigateur vous aurez des ancres sur les différentes parties pour y accéder plus rapidement (à partir du sommaire) et vous trouverez la version complète et intégrale du code sur Github en toute fin de newsletter, ce qui vous permettra de copier coller les bouts de code qui vous intéressent (coucou Florian qui nous avait demandé ça lors de la dernière newsletter. 😉)
Autorisations
Dans le but d'exposer exactement ce qu'on veut à qui on veut, il va falloir commencer par savoir qui est le client qui fait l'appel. Ce qui nous permettra ensuite d'introduire des variations en fonction du client.
Oui, sauf que ça serait quand même un peu exagéré de faire des variations pour chaque utilisateur, alors on va plutôt utiliser un système de rôles qui peut être (ou ne pas être) rattaché aux utilisateurs.
Alors, Allons-y (Alonzo) ! Créons un module d'authentification qui nous sert à récupérer le rôle du client :
Voilà, donc le code prend un header "X-USER-ROLE" et si le header est là, le client est authentifié avec soit le rôle admin, soit le rôle guest. Et on encapsule tout ça dans une OpenStruct afin de simuler un système plus réaliste où on aurait un objet current_user
avec une méthode rôle.
Ensuite, on va ajouter la gem pundit. Elle permet de facilement mettre en place des autorisations au niveau du controller et pour ça, il faut créer une "policy" et donc par exemple pour nos Book :
Pour les actions d'index et de show, on autorise les deux rôles guest et admin à y accéder.
(Pssss, si tu es dans ton navigateur, tu veux agrandir les images avec les petites flèches en haut à droite)
Alors bien sûr pour que tout ça fonctionne il faut qu'on adapte un petit peu le reste du code de l'API. Voici les différentes modifications qu'on a faites :
On ajoute nos sous-classes à la logique de pundit :
On intégre la policy dans la logique générale de l'API :
On ajoute les safeguards de pundit dans notre module d'autorisations pour s'assurer qu'on applique bien toujours la logique définie dans la policy :
Et on gére le nouveau cas d'erreur si un client veut accéder une action qui lui est interdite :
Dans les default_index
, default_show
et default_destroy
, on remplace simplement act_as_jsonapi_model_with_includes
par act_as_jsonapi_scope
afin d'appliquer la contrainte du scope. Et il ne nous reste plus qu'à créer les deux serializer pour nos deux rôles et on sera bons !
Pour le serializer guest, on va exposer le nom, le prix et l'auteur, mais pas l'éditeur :
Et pour le serializer admin, là on veut tout exposer et pour ça on va utiliser une nouvelle méthode trusted_serializer
qui va directement aller regarder le modèle et ajouter tous les attributs et toutes les associations dans notre serializer. Comme ça on est sûr de tout exposer tout le temps, même si notre modèle évolue !
Et voilà ! Alors à quoi ça ressemble ?
Si j'affiche l'index en tant qu'admin, voici la réponse :
Je vois bien les quatre livres, tous les attributs et toutes les associations. Par contre si je fais la même requête mais en tant que guest :
Je ne vois plus tous les livres mais uniquement ceux qui correspondent au scope. Je ne vois plus l'association publisher et dans les meta (pour les environnements de developpement ou de staging) je vois la liste des filtres autorisés.
Mais qu'est-ce qu’il se passerait si j'essayais malgré tout de faire le filou et, toujours en tant que guest, d'accéder à des éléments auxquels je n'ai normalement pas accès ? Si par exemple j'essaye d'utiliser un filtre non permis
http://localhost:3000/api/v1/books?filter[author_name_cont]=Franquin
Ou si j'essaye d'inclure une association à laquelle je n'ai pas accès ?
http://localhost:3000/api/v1/books?include=publisher
Ou si je fais un show sur un record qui existe mais qui est supposé m'être invisible ?
http://localhost:3000/api/v1/books/1
Ou si j'essaye de faire une action que je n'ai pas le droit de faire ? Si je supprime un livre hein ? Il se passe quoi si j'essaye de faire ça ? DELETE
http://localhost:3000/api/v1/books/1
Okay. Cool cool. On a un système d'autorisations plutôt robuste donc. Ça veut dire que désormais on peut différencier, en fonction du le rôle du client, les actions qu'il peut faire, les records qu'il peut voir, les filtres qu'il peut utiliser et les infos qu'il peut récupérer pour un record auquel il a accès. Pas mal !
Et maintenant qu'on vient de personnaliser l'expérience en fonction du rôle, on va voir comment on peut encore plus personnaliser l'expérience avec les filtres avancés !
Advanced Search
"Les filtres ? C'est quoi ce bazar ? Il n'a pas lu son propre article ou quoi ? Il le sait pas qu'il y en a déjà des filtres dans l'API ? WTF ???".
Allons chers lecteurs, du calme. Bien sûr qu'on a déjà des filtres des l'API, cependant ceux qu'on a déjà, bien que très flexibles, ne permettent pas de couvrir tous les scénarios possibles ! Et vous pourriez avoir envie de mettre à disposition des filtres spéficiques pour vos clients.
Par exemple, si on voulait filtrer nos livres en affichant seulement le moins cher pour chaque éditeur (pour afficher sur une page promo pourquoi pas) et bien avec l'API telle quelle, on ne pourrait pas. (Spoiler : à la fin de ce chapitre, on pourra !)
La première étape sera d'ajouter la gem rectify. Un mot d'avertissement cependant, la gem n'a pas été mise à jour depuis 2018 ! Et même si elle fonctionne toujours bien et ne nécessite pas d'update particulière, je ne sais pas si je la recommanderais pour un projet pro.
Nous on va s'en servir dans cet article parce qu'elle nous donne accès à trois types d'objets en une seule fois (ça nous évitera d'avoir à introduire trois gems différentes) : les queries (qui sont des objets qui encapsulent une requête SQL), les forms (qui sont des objets permettant d'effectuer des validations sur un record) et les commands (qui servent à encapsuler et éxécuter uns logique métier particulière).
Bon, maintenant que ça c'est dit, créons notre première query :
Qui nous permet de récupérer le livre le moins cher chez chaque éditeur.
Et juste par bonne mesure, on va aussi créer une query qui a besoin d'un paramètre en entrée (pour celle-là je n'ai pas trouvé d'exemple crédible, du coup cette query fait quelque chose qu'on pourrait faire avec les filtres standards mais je vous fais confiance pour faire comme si) :
Okay et maintenant le but du jeu va être de faire un module à ajouter à notre API et qui nous permettra d'utiliser les queries comme filtres. Quelque chose dans ce genre là peut-être ?
Et voilà ! Le module va récupérer le filtre à partir de l'url, retrouver la query associée, sécuriser et appliquer les paramètres associés (s'il y en a) et exécuter la query.
Une petite astérisque cependant : dans l'état actuel le module ne sais gérer que les paramètres sous forme d'arguments nommés.
Bon et bien sûr il faut l'intégrer au reste de notre API :
Ce qui nous permet de définir les queries à utiliser en tant que filtre au niveau du controller :
Et bien sûr n'oubliez pas d'autoriser ces nouveaux filtres dans la policy associée, mais nous on n'en aura pas besoin puisqu'on va utiliser le rôle admin et comme tous les filtres sont autorisés pour ce rôle, il ne nous reste qu'à utiliser nos nouveaux filtres !
Donc, pour le premier exemple
http://localhost:3000/api/v1/books?filter[cheapests]=1
On récupère bien les livres les moins chers pour chaque éditeur. Et pour le second exemple avec des paramètres
http://localhost:3000/api/v1/books?filter[publisher][client_id]=hatier
On récupère bien tous les livres en fonction de l'id client.
Et si on avait mis un code client plutôt qu'un id client en paramètre ?
http://localhost:3000/api/v1/books?filter[publisher][code]=hatier
Super ! Maintenant on peut offrir à nos clients une nouvelle gamme de filtres ultra personnalisés pour répondre au moindre de leur besoins et ça dans une architecture de code qui ne risque pas de devenir un plat de spaghettis ! Cool ! Très cool même et c'est même pas fini !
C&U
Ça y est ! Enfin ! Le moment que (peut-être) vous attendiez tous (j'espère) ! Le temps du create et de l'update !
Première étape, on va rajouter la configuration dans les routes et dans le controller pour ces deux actions. Pour ça je vous laisse faire vous êtes grands. Ensuite on va modifier notre policy pour n'autoriser le create que pour le rôle admin et par contre on va autoriser l'update pour nos deux rôles !
Dans la policy on va aussi spécifier, comme on l'avait fait pour les sérializer, la commande à utiliser en fonction du rôle du client. (Les commandes c'est un type d'objet fourni par la gem rectify qui nous permet d'encapsuler du code métier. D'autres personnes vont appeler ça des "services objects" mais en gros, on va s'en servir pour permettre de différencier le code exécuté en fonction du rôle et pour mieux organiser nos fichiers.)
Comme on avait fait pour les actions d'index, de show et de destroy, on va créer un modèle par défaut pour nos deux actions de create et d'update :
Et :
J'attire un moment votre regard sur les deux méthodes act_as_jsonapi_validate_request_type_param!
et act_as_jsonapi_validate_request_id_param!
qui nous permettent de valider la structure des requêtes conformément à la norme jsonapi.org.
Forcément ça nous demande de modifier un peu le cœur de notre API :
Et on modifie aussi le module d'autorisations pour s'assurer que les policies sont bien appelées pour les deux actions (au cas où on n'utilisera pas les actions par défaut) :
Ça c'est fait. Maintenant qu'on a le framework pour activer nos actions de create et d’update, tout ce qui nous reste à faire ce sont les commandes pas vrai ?
Pour notre rôle admin, on va faire des commandes génériques. Pour les autres rôles on préférera faire des commandes particulières pour chaque ressource et action, mais comme l'admin a accès à tout, la logique sera la même pour tous nos modèles :
La logique est simple, on prend la ressource (== le record), on utilise un form pour vérifier la validité des paramètres envoyés par le client et si tout est bon on sauvegarde.
Sauf que je ne vous ai pas encore parlé des forms. C'est un autre type d'objet qu'on récupère de la gem rectify et qui nous permet de prendre un set de paramètres et d'appliquer des validations comme ce qu'on a l'habitude de faire dans le modèle.
L'intérêt d'utiliser un form ici, plutôt que des validations dans le modèle, c'est que ça nous permet d'avoir des validations différentes en fonction de la commande utilisée (et donc, en fonction du rôle du client). Vous pourrez vérifier la validité d'un paramètre si le client est un guest mais ne pas être aussi strict si c'est un admin. Voire même ne pas autoriser la même liste de paramètres en fonction du rôle.
Bref, ça vous donne, encore une fois, beaucoup de flexibilité sur ce que vous voulez permettre ou non, en fonction du rôle. Et dans notre cas, pour le rôle admin, notre form ressemble à ça :
On liste les attributs permis, les associations, les validations, rien de bien complexe. Sauf un petit truc : les formulaires de rectify ne donnent pas directement la possibilité d'ajouter les associations alors on a fait un petit monkey patch pour les ajouter. Je vous le mets si jamais vous choisissez d'utiliser cette gem.
Alors qu'est-ce que ça donne ? Il nous suffit d'appeler notre endpoint en tant qu'admin
POST http://localhost:3000/api/v1/books
Haaaaan mais oui forcément, si on met pas les paramètres forcément les validations passent pas ! Bon au moins les messages d'erreurs sont pratiques, ils nous disent bien ce qui ne va pas et où, ça c'est cool. Allez, on recommence et on n’oublie rien cette fois !
Nice ! On a un create qui fonctionne !
On applique la même logique pour l'update. Une commande par défaut et on réutilise le même form que pour le create :
Et on peut tester ça tout de suite
PATCH http://localhost:3000/api/v1/books/5
Et voilà ! Ça fonctionne pour le rôle admin, mais vous n'avez pas oublié, même si ça fait un moment, qu'on avait aussi autorisé l'update pour le rôle guest dans la policy ? Alors il faut qu'on crée la commande pour ça :
Vous avez remarqué ? Pour les guest on ne permet de mettre à jour que le name des livres. Et donc même si dans mon payload je mets d'autres paramètres :
Il n'y a effectivement que le name qui a été mis à jour.
Bon, là il faut quand même reconnaître que le fait qu'il n'y ait pas d'erreur ici alors qu'on a envoyé un paramètre qui n'était pas autorisé, c'est pas tiptop et ça mériterait un fix (que je vous laisse faire à vos heures perdues) !
Et avec ça on a toutes nos actions CRUD ! Après rien ne vous empêche de faire des actions plus custom, surtout qu'on facilite cette voie avec la possibilité d'utiliser les actions par défaut de create et d'update mais avec d'autres commandes qui seraient spécifiques à certaines actions (si vous voulez faire une API avec un design très métier par exemple plutôt que CRUD).
Et avec ça, vous avez maintenant la possibilité, à la fois d'autoriser différents paramètres en fonction du rôle, mais aussi différentes logiques de traitement. Vous voulez envoyer une notif quand un admin fait une action à la place d'un utilisateur mais pas quand l'utilisateur le fait directement ? Vous pouvez ! Vous avez un e-mail obligatoire sur votre application web pour le login, mais c'est un numéro de téléphone sur votre app mobile ? Ben vous pouvez aussi en fait. Voilà, c'est facile.
Alors on a vu les autorisations, les filtres avancés, les deux actions qu'il nous manquait, mais je crois qu'il nous reste quelque chose à faire encore... qu'est-ce que c'était déjà ?
Ha oui ! Les tests !
Tests
C'est vrai que c'est important les tests. Je me souviens d'une époque où ça n'existait presque pas dans Rails et où on avait des bugs et des effets de bords à presque chaque déploiement. Bon, on a progressé depuis et maintenant on code deux fois plus lentement (parce que la moitié du temps c'est pour écrire les tests) mais au moins ça ne bug plus (enfin, c'est ce qu'on dit en tout cas ^^) !
Bref, il est temps d'écrire des tests pour notre API !
Déjà, première info, on va utiliser rspec. Et seconde info, pour se faciliter la vie on va utiliser, beaucoup utiliser, des shared_examples
et des shared_contexts
pour se simplifier la vie et nous permettre d'écrire les tests plus efficacement.
Alors oui, sur un seul fichier ça sera pas forcément pertinent, mais comme, troisième info, on va faire un fichier par ressource et par rôle, ça va vite vous faire pas mal de fichiers et à ce moment là les shared_examples
et shared_contexts
vont venir diminuer la verbosité de vos tests, qui croyez moi seront déjà suffisamment verbeux comme ça !
Bon et puis pour vous présenter le tout, ce qu'on va faire c'est que d'un côté je vais mettre le fichier de spec, de l'autre je mettrai les shared_examples
ou shared_contexts
utilisés et quelques petites explications pour dire qu’est-ce que tout ça fait !
Allez, on commence par la spec pour le rôle admin :
Je vous laisserai vous balader dans le repo (qui est à la fin de cette newsletter) pour découvrir les tests de l’update, du destroy et tous les tests du rôle guest.
Okay ! Bon et maintenant si vous voulez vraiment être exhaustif, vous pouvez aussi faire un fichier où le client n'est pas authentifié (ça peut être pertinent si vous avez une partie de votre API complètement ouverte).
Et voilà ! Vous avez des tests qui, bien qu'améliorables, tiennent quand même pas mal la route 😁 !
Conclusion
Bon, qu'est-ce qu'on retire de tout ça ? Si on résume on a une API qui :
est paginable,
est ordonnable,
est filtrable,
y compris avec des filtres personnalisés,
à la demande du client,
peut inclure les associations dans la même requête,
peut renvoyer une sous-sélection des attributs,
en fonction du rôle,
peut renvoyer différents formats pour le même objet,
peut autoriser différentes actions,
peut autoriser différents filtres,
peut exécuter un code différent pour les actions de création ou de modification
peut autoriser différents paramètres pour les actions de création ou de modification
C'est quand même vraiment bien, en terme de fonctionnalités pour le client et de personnalisation pour le développeur, c'est ce que je connais de mieux. Et pour rajouter la cerise sur le gâteau j'argumenterai que l'architecture que j'ai décrite, avec l'utilisation des policies, queries, commandes, forms et serializers, rendent le tout facilement maintenable. Chaque type d'objet a une responsabilité bien définie et si vous créez des commandes et des forms pour chaque ressource et rôle, vous n'aurez aucun effet de bord à craindre.
Vous pouvez toujours décider de ne pas utiliser les différents objets, ou les rôles de la façon dont je les ai décrits bien entendu. Mon objectif était surtout de présenter une série de gems et quelques bouts de code faits maison pour proposer une architecture. Mais prenez-en tout ou partie et adaptez ça aux autres outils que vous utilisez déjà.
En tout cas, si vous mettez en place tous les éléments que j'ai montrés vous aurez une API vraiment solide, vraiment flexible et vraiment customisable et qui vous catapultera sans aucun doute dans la ligue platine des développeurs backend ! OoO !!
Et pour ceux qui sont toujours là, merci de m'avoir lu. J'espère que vous aurez trouvé ça intéressant et je vous quitte avec un dernier petit cadeau : la version complète et intégrale du code sur Github !
Rendez-vous le mois prochain les biscuitos !
— Thomas
L’agence de recrutement pour les leads dévs RoR !
Comme vous le savez, derrière Ruby Biscuit, il y a Capsens 👋 , nous sommes une agence web qui fait du Ruby on Rails depuis 10 ans.
Avec le temps on s'est rendu compte que beaucoup de dévs choisissent leur entreprise un peu par hasard alors qu'ils pourraient davantage s'épanouir et se valoriser dans des structures qui leur correspondent mieux. De plus on sait à quel point les process de recrutement peuvent ne pas être adaptés à notre métier et nos profils.
Ce qui tombe super bien c'est que chez Capsens nous avons une excellente connaissance de l'écosystème RoR en France, avec un réseau d'entreprises considérable. La plupart étant des boites bien installées (+ de 5 ans), avec des équipes tech déjà présentes et qui recherchent avant tout des leads dévs et dévs séniors.
C'est pourquoi nous avons décidé de mettre à profit nos ressources pour vous aider à trouver le poste de vos rêves !
Alors tu as plusieurs années d’expériences ? Tu souhaites trouver le prochain poste de lead dév de tes rêves ?
Concrètement voilà ce qui va se passer :
Réponds à cette newsletter en te présentant en deux lignes !
Je t’envoie aussitôt notre test technique pour évaluer ta séniorité
Je te propose des créneaux pour un appel afin de faire ta connaissance et que tu me dises ce que tu cherches pour t’épanouir dans une entreprise.
Je te propose 3 entreprises qui correspondent à ton profil et tes aspirations. Pour chacune de ces entreprises :
Je me charge de te donner un max d’infos et répondre à toutes tes questions par message (horaires, ambiance, taille et séniorité de l’équipe, responsabilités, marge de manœuvre pour la négociation du salaire, localisation des bureaux, politique de télétravail, etc). Pas d’appels inutiles.
Avant de rencontrer le recruteur lui-même, je te mets en relation avec un développeur de leur équipe. Tu pourras alors te faire une idée de comment ça se passe de l’intérieur.
Enfin, le recruteur te recevra ! Il aura déjà eu toutes les informations que je lui aurai transmises sur toi ce qui vous permettra d’aller à l’essentiel !
Lance-toi, on attend ton e-mail ! Et si tu aimes déjà ton travail, ne nous contacte surtout pas ! Ou alors fais-le pour nous recommander ta boîte 😉
Mélanie