Clean architecture : contexte
On m'a toujours appris que le clean code (clean architecture, hexagonal, ports & adapters...) était la bonne manière de structurer du code. Avec le recul, je pense que c'est plus nuancé que ça. Certaines approches sont plus adaptées que d'autres selon le contexte technique. Un petit service interne (j'en parle dans ma note sur Hono) n'a pas les mêmes besoins qu'une API qui porte un domaine métier complexe. Oui, "ça dépend du contexte", phrase toute faite numéro 1. C'est un cliché pour une raison : c'est vrai.
Ceci dit, pour les gros projets, genre une API REST conséquente avec un domaine riche et du code qui va vivre des années, la clean architecture reste à mes yeux la meilleure base. C'est un cadre structurant, et quand le projet grossit, on est content de l'avoir posé tôt. C'est justement dans ce cadre-là que j'ai fini par buter sur les limites de TypeORM.
5 ans avec TypeORM
Ça fait au moins cinq ans que j'utilise TypeORM. Il m'a rendu beaucoup de services, et je ne vais pas cracher dessus. C'est un outil qui fait le job pour beaucoup de projets. Mais honnêtement, je l'ai toujours plus ou moins mal utilisé, et je pense que c'est en partie lié à sa conception.
Le souci principal : TypeORM ne facilite pas la séparation entre les objets du domaine et les entités stockées en base.
Pas nativement, en tout cas. On peut bricoler, mais dès qu'on veut faire ça de manière strongly-typée (et j'utilise
TypeScript dans tous mes projets en essayant de le faire correctement, sans any à chaque ligne), ça devient vite
pénible.
En clean architecture, on veut pouvoir manipuler des objets métier sans que ceux-ci soient couplés à la couche de persistence. Avec TypeORM, l'entité est le modèle de base de données. Décorateurs, colonnes, relations : tout est mélangé dans la même classe. Le domaine est collé à la DB. On peut tout à fait faire autrement, séparer les deux, mais du coup on se retrouve à redéclarer ses concepts dans de nouveaux fichiers, encore une fois. Ça alourdit le repo, ça ajoute du boilerplate, et au final on perd une partie de l'intérêt d'utiliser un ORM. C'est pas un défaut de TypeORM en soi, c'est juste que l'outil n'a pas été pensé pour ce cas d'usage.
Le passage par Prisma
J'ai testé Prisma rapidement. L'outil est bien fait, la communauté est solide, et la DX est agréable sur des projets
simples. Mais dans mon contexte, j'ai buté sur un truc : il faut redéfinir ses entités dans un fichier .prisma séparé,
avec son propre DSL.
Dans un projet en clean architecture, ça veut dire que pour chaque concept je dois maintenir :
- Le(s) DTO (entrée/sortie API)
- L'objet du domaine
- Le schéma DB (dans le fichier Prisma)
Trois définitions du même concept. Pour moi, c'est trop. C'est source de désynchronisation, et ça m'a pas semblé être le bon compromis pour mon usage. Je comprends que d'autres s'y retrouvent très bien.
Drizzle, en sceptique
J'ai découvert Drizzle via Twitter. Et comme souvent avec ce qui buzz, j'étais prudent. Voir passer des threads enthousiastes c'est une chose, mais intégrer ça dans des projets en prod qui vont tourner des années, c'est autre chose. J'étais curieux, pas convaincu.
Mais j'ai quand même essayé sur un projet annexe. Et là, j'ai accroché.
Ce que j'apprécie particulièrement :
- Le plein potentiel de Postgres, sans sacrifier le typage. Schémas, fonctions, types custom, vues, vues matérialisées, RLS... Beaucoup d'ORM n'intègrent pas ces features (ou pas nativement) parce qu'elles ne sont pas standard entre les DB SQL. Sauf que ce sont des outils super utiles dans beaucoup de cas, et Drizzle les supporte. En plus, on peut écrire du SQL typé quand le query builder ne suffit pas. Avoir tout ça dans le même outil, c'est exactement ce qu'il me manquait.
- Une approche sans classes. Avec TypeORM, tout passe par des classes décorées. Avec Drizzle, les schémas sont des objets, les requêtes retournent des objets typés. C'est plus léger, plus direct.
- Le schéma est la source de vérité pour les types. Le schéma Drizzle génère directement les types TypeScript. Un
selectretourne exactement ce qu'on attend. Pas de cast, pas de generics alambiqués, pas de fichier de types à maintenir à côté.
Rendre aux repositories leur responsabilité
Un truc qui me gênait depuis longtemps avec TypeORM, c'est ce que mes repositories étaient devenus. Dans la pratique,
ils finissaient presque systématiquement en adaptateurs CRUD génériques : create, update, getById, delete.
Quatre méthodes, toujours les mêmes, sans vraie valeur ajoutée. Le repository n'avait aucun rôle métier, il ne faisait
que relayer des appels à la base.
Avec Drizzle, j'ai pu changer ça. Comme les requêtes sont flexibles et typées, j'expose des interfaces qui ont du sens
pour le domaine. Pas un getById suivi de trois getById sur des entités liées, mais une vraie méthode qui retourne un
objet cohérent, prêt à être utilisé par le service.
Et c'est là que j'ai enfin réussi à intégrer proprement les agrégats dans mes projets. L'idée est simple : quand des entités sont intrinsèquement liées (une commande et ses lignes, un utilisateur et ses préférences), c'est le repository qui les charge et les retourne ensemble, comme un tout. Le domaine reçoit un objet complet. Il n'a plus à gérer la mécanique de reconstruction, il n'a plus à se demander si tel champ est chargé ou pas.
Concrètement, ça élimine toute une catégorie de cas absurdes. Plus de vérifications défensives dans le domaine pour couvrir des états qui n'auraient jamais dû exister. Le repository garantit la cohérence de ce qu'il retourne, et le domaine peut se concentrer sur ce qu'il sait faire : les règles métier.
C'est un step-up qui peut sembler subtil, mais dans mon expérience, ça change pas mal la qualité du code au quotidien.
Sur le paradigme des classes
Il y a une tendance que j'observe depuis un moment : le développement par classes, à la NestJS + TypeORM, semble perdre un peu de terrain dans l'écosystème JS/TS. Je dis pas que c'est bien ou mal, c'est juste un constat.
Le modèle est très inspiré de Java : héritage, décorateurs, injection de dépendances par constructeur. Ça fonctionne, et ça apporte une structure rassurante sur les gros projets. J'utilise encore NestJS au quotidien et je continuerai probablement.
Mais JavaScript, à la base, c'est des objets et des prototypes. Les classes sont un ajout syntaxique, pas le fondement du langage. Drizzle, à sa manière, va plus dans cette direction : des fonctions, des objets, du TypeScript idiomatique. C'est pas forcément mieux dans l'absolu, mais pour la couche data, ça colle mieux à ce que j'attends.
TypeORM reste un bon outil. Drizzle correspond juste mieux à la façon dont je structure mes projets aujourd'hui.
Pour conclure
Je n'ai pas migré tous mes projets. Ceux qui tournent bien sur TypeORM y restent, et c'est très bien comme ça. Mais pour les nouveaux projets, et surtout ceux où la clean architecture a du sens, Drizzle est devenu mon choix par défaut. Meilleur typage, requêtes plus flexibles, repositories avec un vrai rôle, agrégats propres. Au quotidien, j'écris moins de code pour un résultat plus solide. C'est difficile de revenir en arrière après ça.
"dependencies": {
- "typeorm": "^0.3.20", // merci pour ces années
- "reflect-metadata": "^0.2.2", // tu pars avec lui
+ "drizzle-orm": "^0.36.0", // bienvenue
},
"devDependencies": {
+ "drizzle-kit": "^0.30.0", // et ramène tes potes
}