Cet article est le second d’une série de deux billets sur la migration de Front Desk vers Rust. Le premier expliquait Migrer un codebase Django de 7 ans vers Rust.
Dans le billet précédent, j’ai raconté ce que Django m’avait apporté, ce qui a fini par coincer, et pourquoi j’ai choisi de réécrire plutôt que de refactorer Front Desk.
Reste la partie la plus intéressante, et celle qu’on lit rarement : comment on s’y prend concrètement. Comment on décide de ce qu’on garde et de ce qu’on jette, quelle architecture on adopte quand plus aucun framework ne la choisit à votre place, quelle place occupent réellement les agents IA dans un chantier pareil, et surtout ce que ça coûte.
L’architecture que Django ne m’imposait pas
Chaque module du nouveau backend suit le même découpage. Le repository parle à la base de données et ne fait rien d’autre. Le service orchestre le repository (une sorte de manager en Django) et les effets de bord, comme l’envoi d’une notification ou d’événements. Le handler (une vue en Django) se limite à l’entrée et à la sortie HTTP. Le handler appelle le service ou repository directement, le service quand la logique est complexe, le repository quand on souhaite simplement interoger la DB.
Il n’y a là rien de nouveau, et surtout rien de propre à Rust. Le pattern est très normé, et il se transpose parfaitement ailleurs, je l’utilise d’ailleurs avec FastAPI et Flutter : en Django, le manager peut très bien tenir le rôle du repository, le service orchestrer, et la vue se contenter d’appeler le repo/service. En FastAPI, c’est la même chose. C’est d’ailleurs son intérêt principal : une fois qu’on a compris ses avantages et ses limites, on garde ses repères en changeant de langage, et on obtient un code clair et facilement testable.
Ce découpage n’a rien d’une abstraction, et c’est important pour la suite : c’est une convention plate, trois emplacements toujours identiques qu’on connaît d’avance. Elle ne met aucune profondeur entre l’intention et le code, elle en retire. C’est l’inverse d’une couche posée par précaution.
Ma façon de coder a changé
Il y a quelques années, j’adorais abstraire. Séparer les responsabilités au maximum, factoriser dès que deux bouts de code se ressemblaient, découper en modules toujours plus fins. Sur le papier c’est propre, et ça respecte les principes qu’on lit partout. En pratique, ça donne un code éclaté dans une dizaine de fichiers, où suivre le chemin d’une requête demande de sauter de module en module. La logique était bien rangée, mais elle n’était plus lisible.
Django facilite l’abstraction et la culture autour la valorise : mixins, vues génériques, héritage à plusieurs étages, tout invite à découper et à factoriser. Le choix restait le mien, mais l’écosystème pousse dans cette direction, et ces patterns sont difficiles à bien utiliser, ils finissent souvent par pénaliser la lisibilité du codebase.
Aujourd’hui je privilégie la localité, la composition et l’approche fonctionnelle. Si un bout de logique n’existe qu’à un seul endroit, il reste dans son contexte (ex. le même fichier) plutôt que de partir dans un sous-module à lui. J’évite l’abstraction tant que je peux : une simple fonction qui prend un paramètre plutôt qu’une classe, et le moins de couches possible entre l’intention et le code qui l’exécute. L’abstraction a une vraie valeur quand le code est réellement réutilisé, mais quand elle est posée par précaution, elle coûte en lisibilité et rend les tests atomiques plus difficiles à écrire.
Le passage à Rust va dans ce sens, puisque l’héritage n’existe pas et que la tentation de la hiérarchie de classes disparaît d’elle-même. Ca se révèle utile pour une raison que je n’avais pas anticipée : un agent IA lit du code local bien mieux qu’un code dispersé. Moins il a de fichiers à traverser pour comprendre ce qu’il modifie, meilleur est sa comprhension et la mienne.
Le front : mon revirement sur HTMX
J’ai longtemps été un ambassadeur de HTMX. J’ai même écrit ici pourquoi je l’avais choisi plutôt que React ou Vue. C’était, pour moi, un pied de nez salutaire à la folie du SPA.
Force est de constater que ça ne tient pas pour ce que je construis. Dès qu’on dépasse le CRUD simple, HTMX montre ses limites. Un outil de travail utilisé toute la journée demande des composants réutilisables, une vraie interactivité, et l’accès aux bibliothèques d’aujourd’hui comme Tailwind ou shadcn.
Les vrais problèmes sont plus concrets que ça. Dès qu’une bibliothèque JavaScript manipule le DOM de son côté, elle se retrouve en conflit avec HTMX, qui remplace des morceaux de page dans son dos : la bibliothèque ne sait plus que le contenu a changé, ni si son script doit se réinitialiser. La navigation arrière pose le même genre de souci, avec des retours de page qui réaffichent un fragment au lieu de la page complète. Chacun de ces cas se règle à la main, et à la longue, gérer soi-même ce qu’un framework front fait nativement devient pénible.
Je suis donc passé à Svelte, et c’est une vraie joie. TypeScript évite une bonne partie des erreurs d’interface, c’est rapide, c’est simple, et comme l’écosystème est très conventionnel, les agents y travaillent remarquablement bien, ce qui compte maintenant que je développe avec eux.
Le plus amusant, c’est que Svelte est à mes yeux le véritable héritier de ce que promettait HTMX : rendre le front accessible sans expédier des mégaoctets de JavaScript au navigateur, je rappelle que HTMX est en soit un “framework” JavaScript dans le sens où il est écrit en JS.
Sept ans de données de production
La tentation, quand on réécrit un système, c’est de le porter à l’identique : chaque modèle, chaque champ, chaque écran. C’est rassurant, mais ça revient à réécrire consciencieusement, dans un langage exigeant, sept ans de décisions dont une partie était mauvaise et dont une autre n’a jamais servi.
J’avais un avantage que je n’exploitais pas : la base de production. Avant d’écrire le moindre domaine en Rust, j’ai restauré un dump et je suis allé mesurer, table par table et colonne par colonne, ce qui était réellement utilisé.
L’adoption des modules est très déséquilibrée : le cahier de consignes est utilisé par environ trois quarts des établissements réellement actifs, quand d’autres modules, sur lesquels j’avais passé un temps considérable, plafonnent autour de 10 %. Et une grande partie des champs de mes modèles est vide, beaucoup étant remplis sur moins de 2 % des lignes, sur des centaines de milliers d’enregistrements et des années d’historique.
Ce sont des champs morts : pour beaucoup, ce sont les demandes acceptées il y a des années pour ne pas perdre un client, que le client en question n’a lui-même jamais utilisées. Parfois c’est un parcours entier qui l’est : les champs qui servaient à prévenir le client qu’on avait retrouvé son objet, puis à le lui expédier, sont tous sous le seuil, en bloc.
D’où la règle que j’ai écrite noir sur blanc dans la doc du nouveau projet : ne pas porter le mort. Un champ mesuré sous le seuil est coupé par défaut, sauf s’il relève du légal, du RGPD, de la sécurité ou de la comptabilité, la rareté d’usage n’autorisant pas à supprimer une trace d’audit.
Reste un piège, et il est sérieux : un chiffre dit qu’une fonctionnalité n’est pas utilisée, jamais pourquoi. Est-elle inutile, ou simplement mal placée, mal expliquée, trop pénible à déclencher ? Les deux cas donnent exactement la même colonne vide, et ils appellent des décisions opposées, couper d’un côté, retravailler l’ergonomie de l’autre.
La donnée seule ne tranche pas. Il faut retourner voir les utilisateurs : sondages, rendez-vous en tête-à-tête, questions ciblées sur les fonctionnalités qui semblent mortes. C’est ce que j’ai fait régulièrement, en sachant que sur un petit échantillon, on n’obtient jamais quelque chose de statistiquement solide. Au bout du compte, le dernier mot revient à l’expérience de la personne qui connaît le produit et sait où elle veut l’emmener.
Le gain n’est pas seulement technique. Front Desk s’adresse à des non-experts : un réceptionniste au standard, un technicien sur le terrain, une femme de chambre. Chaque champ coupé est un champ de moins à comprendre pour quelqu’un qui est interrompu toutes les trente secondes. Le bloat est autant de la friction d’usage qu’une dette technique. La migration est donc devenue une remise à plat du produit, avec la donnée comme premier arbitre plutôt que mon seul instinct. Elle ne remplace pas le jugement pour autant, elle le contraint : elle désigne ce qui mérite d’être rouvert, l’expérience et les retours utilisateurs décident du reste.
Le rôle des agents
J’utilise ces outils depuis longtemps. J’étais parmi les bêta-testeurs de GitHub Copilot en 2021, à l’époque où il s’agissait d’autocomplétion, franchement utile sur un snippet et pas beaucoup plus. Ça n’a plus rien à voir. Depuis Sonnet 4.6, je considère qu’un cap est passé : avec les bonnes instructions et une architecture claire au départ, le code produit est excellent, souvent équivalent au mien, souvent meilleur.
Une migration de cette taille en solo, c’est une montagne que je n’aurais jamais entrepris avant, et une bonne partie est ennuyeuse : mesurer l’usage d’une colonne, écrire les requêtes SQL d’audit, transposer un schéma, vérifier la parité de comportement entre l’ancien et le nouveau système.
Tout ce qui est mécanique et me fait perdre du temps sans me faire progresser, je le délègue à des agents IA. Je garde la main sur chaque ligne de code et sur l’architecture ; les agents ne décident pas de la forme du système, ils exécutent des procédures que j’ai écrites. Ce qui rend ça tenable, c’est de capturer les tâches répétables (auditer l’usage en prod, vérifier la conformité d’une migration, auditer les dépendances) comme des procédures versionnées dans le repo, d’écrire les règles d’architecture une fois pour toutes dans un fichier chargé à chaque session, et de ne considérer aucun changement comme terminé tant que le formatage, les lints, les tests et la vérification des requêtes SQL ne passent pas. C’est un hook git qui le vérifie, pas ma bonne volonté.
Rust se prête bien à cet exercice, justement parce qu’il est exigeant, boring dans le bon sens, et parce qu’il produit des messages d’erreur précis. Quand un agent invente un champ inexistant, oublie une variante d’enum ou se trompe de type, le compilateur le lui dit avant que le programme ne tourne. L’agent lit l’erreur, corrige, recompile, et recommence jusqu’à ce que ça passe. Il ne me rend pas du code approximatif qui compile mal, il me rend du code qui compile, et il a fait le tour des erreurs de type tout seul. Ça ne garantit évidemment pas que la logique métier soit juste, c’est mon travail de le vérifier, mais toute une classe d’erreurs est éliminée avant que je ne relise quoi que ce soit. En Python, ce même code s’exécute tranquillement jusqu’au None inattendu, trois semaines plus tard, en production.
Reste à savoir où se situe la valeur du développeur là-dedans. Pour moi, elle s’est déplacée : elle est dans l’architecture, dans la façon de brancher les morceaux ensemble, de faire tenir l’ensemble et d’optimiser le codebase. Le vrai travail a toujours été de produire du code avec moins de bugs.
Soyons clair, j’ai passé plus de 2 semaines à créer le squelette et l’architecture du projet, en faisant du back an forth avec l’agent, pendant des semaines j’ai du corriger l’architecture, certains choix techniques, mais le code en lui même était toujours “correct”. Les plus grosses erreurs venaient souvent d’une intépretration de la business value, de la vision du produit et du scope.
C’est pour ça que l’idée qu’on pourrait écrire des millions de lignes par semaine en gardant la qualité d’avant me paraît fausse. Le goulot d’étranglement a simplement changé de place. Avant, la limite c’était le nombre de lignes qu’on arrivait à écrire dans une journée. Aujourd’hui, c’est le nombre de lignes qu’on arrive à relire et à comprendre sérieusement sans y laisser toute son attention. La bande passante disponible, la concentration, la capacité d’analyse : voilà ce qui plafonne. Et c’est un piège pour qui n’a pas encore compris que l’IA ne remplace pas le développeur. Peut-être qu’un jour on ne lira plus le code du tout, et là tout sera différent, mais ce n’est pas encore le cas.
Ça rejoint tout le reste de ce chantier. L’IA travaille sur du contexte : moins il y a de code, plus il est simple à lire et à parcourir, mieux je le comprends, et mieux l’agent le comprend aussi. Couper les champs morts, éviter les abstractions inutiles, garder la logique là où on l’utilise, c’est de l’hygiène de développeur, et c’est aussi ce qui rend le code lisible par la machine à qui je le confie.
Bien câblée, bien dirigée, bien orchestrée, l’IA m’apporte une vraie valeur au quotidien, et je vois mal comment en douter aujourd’hui. Je ne suis pas le seul à le penser : Linus Torvalds défend lui aussi l’IA au sein de Linux.
Combien ça coûte
La question qu’on me pose, c’est le prix. Elle est légitime, parce que le seul exemple public de cette taille est celui de Bun, qui a porté son codebase de Zig vers Rust : environ un million de lignes en 11 jours, pour à peu près 165 000 $ de tokens au tarif API. De quoi refroidir n’importe quel développeur solo.
Sauf que ce n’est pas le même exercice. Bun a porté l’intégralité d’un runtime JavaScript, en s’appuyant sur sa suite de tests existante pour valider le résultat. Moi, je ne porte pas tout, loin de là : environ 60 % de ce qui tournait sous Django reste sur le carreau, entre le framework, ses paquets et mes fonctionnalités mortes. De ce qui méritait d’être gardé, j’estime avoir porté 90 %, et ce qui passe est retravaillé au passage, la logique raffinée, les garde-fous ajoutés, les modèles repensés.
Ça m’a pris un mois, à raison d’environ une heure et demie par jour, plus trois ou quatre heures le week-end.
Concrètement, je m’en sors avec un abonnement à 19 € par mois. J’atteins les limites d’usage régulièrement, mais ça suffit. On n’a pas besoin de mille ou deux mille euros par mois pour refaire un SaaS. Je précise quand même une chose : je pars d’un codebase existant, qui joue le rôle de spécification. C’est un avantage énorme, et ça ne se compare pas à partir d’une page blanche.
Et le poste de dépense principal est souvent mal identifié. Ce qui prend du temps, c’est ce qu’il faut écrire avant que l’agent travaille : les documents d’architecture, les longues explications sur le pourquoi des décisions, les procédures. Une fois que c’est posé, l’agent avance seul et je relis. Le coût de ce chantier se compte en temps d’écriture et de relecture, bien plus qu’en tokens.
Ce que je retiens
Une migration de cette ampleur n’est pas une traduction. C’est une occasion de rouvrir toutes les décisions qu’on a prises sans savoir, et de les reprendre avec ce qu’on sait maintenant, y compris ce que la base de données nous apprend sur nos propres illusions.
Le langage compte moins que la méthode. Mesurer avant de porter, refuser de traîner le mort, garder le code local et lisible, écrire ce qu’on attend avant de déléguer, et relire tout ce qui revient. Rust rend ces règles plus faciles à tenir parce qu’il ne pardonne rien, mais rien n’empêche de les appliquer ailleurs.
