Turborepo monorepo : types partagés et SDKs générées automatiquement
Comment organiser plusieurs apps (React Native, Next.js, FastAPI) avec des types partagés et des SDKs OpenAPI générées.

Quand une équipe produit fait coexister une API Python, deux apps React Native et une console Next.js, la première douleur n'est pas l'architecture des services — c'est la dérive entre ce que le serveur retourne et ce que les clients croient recevoir. Une équipe que nous accompagnons a remonté ce problème de façon limpide : un changement de schéma sur l'endpoint `/v1/rides` a provoqué pendant trois jours un bug intermittent en production sur l'app passager, parce que le type TypeScript côté mobile était une copie figée écrite à la main six mois plus tôt. La cause n'était pas une faute individuelle ; c'était l'absence d'une source de vérité partagée. Turborepo répond bien à ce problème, à condition de structurer correctement les packages internes. La règle qui paye le plus : un seul package `@org/types` qui contient les schémas Zod canoniques (ou Pydantic + zod-from-pydantic si le backend est en Python), exporté à la fois comme types TypeScript et comme validateurs runtime. Tous les autres packages — API client, hooks de data fetching, mock servers de test — dépendent de `@org/types` et jamais l'inverse. Cette discipline élimine 80% des dérives, parce qu'un changement de schéma casse immédiatement la compilation du package client, qui casse immédiatement la compilation des apps consommatrices. Le deuxième pilier est la génération automatique des SDKs clients. Plutôt que d'écrire à la main un wrapper fetch typé pour chaque endpoint, nous exposons l'OpenAPI 3.1 côté FastAPI (gratuit en Python) et faisons tourner `openapi-typescript` ou `@hey-api/openapi-ts` dans un job Turborepo dédié au moment du build. Le package `@org/api-client` est régénéré, les apps consommatrices recompilent contre lui, et n'importe quel changement de contrat invalide la build. Sur un projet réel, cela a divisé par cinq le temps moyen passé en debug d'écart serveur / client. Les dépendances entre packages internes sont la principale source de complexité émergente. Une règle simple suffit : un graphe en couches strict — `types` → `validation` → `api-client` → `domain-logic` → `apps`. Aucune flèche en arrière. Turborepo permet de matérialiser cette discipline par les `dependsOn` dans `turbo.json` : le pipeline de build refuse une dépendance circulaire dès la première compilation. Ajouter un linter type `dependency-cruiser` côté CI verrouille définitivement la propriété. Le cache distant Turborepo est le levier de performance que beaucoup d'équipes sous-utilisent. Sans cache distant, un `turbo build` après un pull qui touche `@org/types` recompile tout le graphe ; avec cache distant Vercel ou self-hosted (Turbo Remote Cache sur S3 ou via le serveur open-source `turborepo-remote-cache`), 80 à 90% des targets sont restorées en quelques secondes. Le piège classique : configurer mal les `inputs` dans `turbo.json`, ce qui rend le cache miss inévitable parce que la moindre modification de README invalide le build d'une app. Auditer les inputs régulièrement vaut largement l'effort. Le versioning interne mérite une décision explicite. Deux écoles cohabitent : `workspace:*` (le client utilise toujours la dernière version locale, simple mais empêche la publication isolée d'un package) ou Changesets avec versionnement strict (chaque package interne a son `0.x.y`, on peut publier `@org/api-client` sur un registry privé). Pour 90% des projets que nous voyons, `workspace:*` couplé à des PR atomiques suffit ; Changesets devient nécessaire quand un package interne est consommé par plusieurs repos ou par des prestataires externes. Un dernier piège mérite attention : la cohabitation TypeScript / React Native / Metro bundler. Metro ne respecte pas `paths` dans `tsconfig.json` sans configuration explicite via `metro.config.js`. Sans cela, l'app mobile compile mais échoue à l'exécution sur les imports `@org/...`. La solution standard est `react-native-monorepo-tools` ou une config manuelle qui ajoute `extraNodeModules` et `watchFolders` pointant vers la racine du monorepo. Idem côté Next.js : `transpilePackages: ['@org/types', '@org/api-client']` est obligatoire dans `next.config.js` pour que le TypeScript des packages soit compilé côté Next plutôt qu'attendu en JS pré-compilé. Le résultat tangible sur une stack React Native + Next.js + FastAPI bien structurée : un changement de champ obligatoire dans l'API casse la build localement en moins de 20 secondes, propage l'erreur exactement aux trois ou quatre fichiers concernés, et empêche le merge tant que les apps n'ont pas été adaptées. Le bug intermittent de trois jours décrit en introduction devient une erreur de compilation détectée à la première Pull Request — c'est le seul résultat qui compte vraiment.
— · —
← Retour au journal