On a démarré Réno Claire fin 2023 — un simulateur de rénovation énergétique qui remplace les tableurs Excel d’un acteur de la réno en Île-de-France. Dix-huit mois plus tard, l’application est en production, utilisée quotidiennement par des agents immobiliers, et elle a survécu à deux montées de version majeures du framework, trois changements de barèmes réglementaires, et une refonte complète du pipeline d’intelligence documentaire.
Ce qui m’a le plus surpris, c’est ce qui a tenu : pas les choix de framework, pas l’architecture — les tests. Le TDD qu’on a appliqué dès le premier jour sur les services métier s’est avéré être l’investissement le plus rentable du projet. Et de loin.
Les formules Excel, ou pourquoi on a fait du TDD
Le client avait des tableurs avec des formules imbriquées sur trois niveaux, des SI() en cascade, des ARRONDI() avec des coefficients empiriques, et des cellules référencées dans huit feuilles différentes. Personne dans l’entreprise ne savait exactement pourquoi certaines constantes avaient ces valeurs — elles venaient de barèmes réglementaires mis à jour au fil des ans, parfois approximés, parfois corrigés à la main.
Notre contrat était simple : les résultats de l’application doivent être identiques à ceux du tableur, au centime près. Pas “à peu près équivalents” — identiques. Un agent qui compare le résultat de l’app avec son ancien tableur ne doit voir aucune différence.
C’est le cas d’école du TDD.
Le cycle était systématique :
- Prendre une formule Excel et la comprendre — parfois le plus long, surtout quand une cellule référence un
RECHERCHEVdans une autre feuille - Extraire les valeurs de test directement du tableur : pour des inputs donnés, noter les outputs attendus
- Écrire le test avec ces valeurs comme fixtures
- Implémenter la fonction TypeScript pour faire passer le test
- Refactorer : renommer les variables cryptiques (
B6,B7) en noms explicites, extraire les constantes
La formule de calcul du nombre de menuiseries ITE, par exemple :
=SI(B3="";0;ARRONDI((-B6*0,3*B3*1,35-B7*0,3*B3*1,35
+B3*1,35*(B12+B19+B25)/B9/2,5);0))
Le test correspondant :
describe("computeNumITE", () => {
it("calculates ITE count for a standard 90m² house", () => {
const property = buildPropertyS0({
surface: 90,
numExteriorDoors: 2,
numExteriorWindows: 8,
level0Surface: 45,
level1Surface: 45,
level2Surface: 0,
ceilingHeight: 2.5,
});
const result = service.computeNumITE(property);
expect(result).toBe(73); // valeur du tableur
});
it("returns 0 when surface is empty", () => {
const property = buildPropertyS0({ surface: null });
expect(service.computeNumITE(property)).toBe(0);
});
});
On a couvert comme ça le WorkCostService (chaque type de travaux : menuiseries, isolation, VMC, PAC, CET…), le SubventionsService (calcul des aides selon la couleur du foyer et les sauts DPE), le HomeColorDetector (détermination de la couleur selon département, revenus, taille du foyer), et le PropertyS0AdditionalFieldsService (surfaces d’isolation dérivées).
Au total, une soixantaine de tests unitaires sur les services métier. Ça représente peut-être deux jours de travail en plus au démarrage. Ce que ces tests nous ont fait économiser ensuite, c’est sans commune mesure.
Le premier changement de barèmes
Trois mois après le lancement, le client nous envoie un email : “Les plafonds MaPrimeRénov’ ont changé, voici le nouveau tableau.” Nouveau plafond de 70 000 € au lieu de 55 000 € pour les foyers éligibles au bonus, nouveaux taux par couleur.
Sans tests, cette mise à jour serait un exercice de haute voltige. On modifie des constantes dans le SubventionsService, on prie pour ne rien casser, et on demande au client de revérifier quelques simulations à la main. Avec tests, c’est mécanique :
- Mettre à jour les constantes dans le code
- Mettre à jour les valeurs attendues dans les tests (avec les nouvelles valeurs fournies par le client)
- Lancer
npm test - Corriger les cas qui échouent (souvent des cas limites qu’on n’aurait pas pensé à vérifier manuellement)
- Déployer
Le tout en moins d’une heure, avec la certitude que les calculs sont corrects. On a fait ça trois fois en dix-huit mois. À la troisième, le client a arrêté de vérifier derrière nous — les résultats étaient toujours justes.
La montée Next.js 14 → 15
En octobre 2024, Next.js 15 sort avec des changements structurants. Le plus impactant pour nous : le caching par défaut est inversé. Les requêtes fetch, les GET Route Handlers et les navigations client ne sont plus cachés automatiquement. C’est l’exact inverse du comportement de Next.js 14.
Sur le papier, c’est un breaking change majeur. En pratique, pour notre application, c’est une bonne nouvelle — on avait passé du temps en Next.js 14.2 à configurer les staleTimes pour désactiver le cache agressif qui nous posait problème sur les formulaires. Avec Next.js 15, le cache est opt-in. Notre code devient plus simple, pas plus complexe.
Mais la montée de version ne s’est pas faite en un clic. Trois sujets ont demandé de l’attention :
React 19 et les API asynchrones
Next.js 15 utilise React 19 (d’abord en RC, stabilisé avec Next.js 15.1 en décembre 2024). React 19 rend certaines API qui étaient synchrones en asynchrones — notamment les cookies et les headers dans les composants serveur. Concrètement, notre middleware d’authentification et nos Server Actions qui lisaient les cookies de session ont dû être adaptés :
// Avant (Next.js 14)
const cookieStore = cookies();
const token = cookieStore.get("session");
// Après (Next.js 15 / React 19)
const cookieStore = await cookies();
const token = cookieStore.get("session");
C’est un changement mécanique, mais il touche des dizaines de fichiers. TypeScript l’a attrapé partout — les types de retour avaient changé, et le compilateur refusait de continuer sans les await.
NextAuth v5 — toujours en beta
NextAuth v5 est toujours en beta en juillet 2025. Ça n’a pas été un problème de stabilité — l’authentification fonctionne sans souci depuis le premier jour. Mais chaque montée de version mineure de Next.js a tendance à casser quelque chose dans l’intégration NextAuth. Les callbacks authorized, jwt et session ont changé de signature entre les betas.
On a dû adapter notre middleware trois fois en dix-huit mois. Ce n’est pas dramatique, mais c’est le type de dette technique invisible qui s’accumule quand on utilise une dépendance en beta sur un projet long terme.
Turbopack en dev : le gain de DX
Next.js 15 stabilise Turbopack pour le développement. On l’utilisait déjà en RC depuis la 14.2, mais avec la stabilisation, c’est devenu notre défaut sans réserve. Le serveur de dev démarre en 700 ms au lieu de 4 secondes, et le Fast Refresh est quasi instantané. Sur un projet avec une centaine de composants et une quarantaine de routes, c’est un gain de productivité tangible au quotidien.
Et les tests dans tout ça ?
La montée de version Next.js 14 → 15 a pris deux jours. La majorité du temps a été passée sur les changements d’API (cookies asynchrones, NextAuth). La logique métier — les services, les calculs, les simulations — n’a pas bougé d’une ligne. Les tests passaient au vert dès que la plomberie Next.js/React était adaptée.
C’est là que le TDD a prouvé sa valeur de façon spectaculaire. Sans tests, on aurait passé des jours à vérifier manuellement que les simulations produisaient toujours les bons résultats après la montée de version. Avec tests, on sait en trente secondes que le moteur de calcul est intact. Le refactoring est sans risque.
La coordination de deux IAs
Le pipeline d’analyse documentaire est la partie la plus ambitieuse du projet. L’idée : un agent scanne le DPE d’un bien, le télécharge, et l’application en extrait automatiquement les données pour pré-remplir le formulaire de simulation.
Le pipeline coordonne deux services d’IA complémentaires :
Google Document AI — l’OCR
Google Document AI fait de l’OCR sur les PDF scannés. On lui envoie le fichier en base64, il retourne le texte brut extrait. C’est un service mature (GA depuis 2021), fiable, et rapide — en moyenne 5 à 10 secondes par document.
Le DPE est un document standardisé, mais les scans sont de qualité variable : photos de travers prises au téléphone, PDF issus de photocopieurs des années 2010, documents annotés à la main. Document AI gère la plupart de ces cas correctement, mais le texte extrait contient souvent du bruit — caractères mal reconnus, sauts de ligne incohérents, tableaux aplatis en texte linéaire.
C’est pour ça qu’on ne parse pas le texte brut avec des regex. On le donne à GPT-4.
OpenAI GPT-4 — l’extraction structurée
GPT-4 reçoit le texte brut du DPE et un schéma JSON décrivant exactement ce qu’on veut extraire :
const schema = {
dpeNumber: "numéro du DPE",
streetAddress: "adresse",
postalCode: "code postal",
city: "ville",
surface: "surface habitable en m²",
yearOfConstruction: "année de construction",
energySource1: "source d'énergie principale (biomass|electricity|gas|fuel)",
dpeClass: "classe DPE (A à G)",
exteriorWallsSurface: "surface des murs extérieurs en m²",
lostAtticsSurface: "surface des combles perdus en m²",
// ...
};
Le modèle retourne un JSON que l’on valide avec Zod. Si un champ est absent ou incohérent, Zod le capture et on tombe en fallback avec une valeur par défaut — l’agent verra le champ vide et le remplira manuellement.
Depuis août 2024, OpenAI a introduit les Structured Outputs — un mode qui garantit que la réponse respecte un schéma JSON fourni. On a migré vers ce mode dès qu’il est sorti. Avant, on envoyait le schéma dans le prompt et on espérait que le modèle le respecte (avec le JSON mode de novembre 2023 pour au moins obtenir du JSON valide). Maintenant, la conformité au schéma est garantie par l’API. Plus besoin de parser des réponses mal formées.
L’orchestration
Le pipeline est géré par une machine à états en base :
uploading → uploaded → ocr-analyzing → ocr-analyzed → ai-analyzing → ai-analyzed
Chaque transition est une mise à jour atomique en base. Le frontend poll le statut et affiche une progression visuelle. En cas d’erreur à n’importe quelle étape, le statut passe à error et l’agent est informé.
La coordination entre les deux IAs est séquentielle, pas parallèle — Document AI doit finir l’OCR avant qu’on puisse envoyer le texte à GPT-4. Le temps total est de 15 à 40 secondes selon la taille du document. C’est acceptable pour un processus qu’on fait une fois par bien, pas à chaque page.
Le timeout des fonctions Vercel est configuré à 300 secondes pour ces routes dans vercel.json. On n’a jamais atteint cette limite, mais les traitements IA sont imprévisibles — mieux vaut une marge confortable qu’un timeout en production.
Les hallucinations
GPT-4 ne hallucine pas souvent sur de l’extraction de données factuelles depuis un document — c’est un cas d’usage où les LLMs excellent. Mais “pas souvent” n’est pas “jamais”. On a observé trois types de problèmes :
- Inversion de champs : la surface des combles perdus confondue avec la surface des rampants de toiture. Ça arrive quand le DPE a une mise en page inhabituelle.
- Valeurs inventées : rarement, mais ça arrive quand le document est partiellement illisible. Le modèle “complète” avec des valeurs plausibles au lieu de retourner null.
- Entités HTML : les premières versions du service avaient un bug où GPT-4 retournait des
&et'dans les chaînes. On a ajouté un post-processing d’échappement.
La solution : ne jamais faire confiance au résultat sans validation humaine. Les données extraites pré-remplissent le formulaire, mais l’agent doit confirmer chaque écran. C’est un assistant, pas un remplaçant.
Les formules Excel — le détail qui change tout
L’aspect le plus sous-estimé du projet, c’est la traduction des formules Excel en code. Pas parce que c’est techniquement complexe — c’est de l’arithmétique. Mais parce que les tableurs Excel sont un langage de programmation implicite, sans tests, sans versioning, et sans documentation.
Prenons le HomeColorDetector. C’est le service qui détermine la “couleur” du foyer (bleu, jaune, violet, rose) en fonction de trois paramètres : le département, le nombre de personnes dans le foyer, et le revenu annuel. Cette couleur détermine ensuite le taux de subvention. En Excel, c’est un RECHERCHEV dans un tableau avec des seuils différents pour l’Île-de-France et le reste de la France, et des montants supplémentaires par personne au-delà de cinq.
En TypeScript, c’est un fichier de configuration JSON avec les tables de seuils, et un service qui fait la recherche :
export class HomeColorDetector {
constructor(private config: HomeColorConfig) {}
detect(
departmentCode: string,
numPeople: number,
annualIncome: number
): HomeColor {
const isIDF = this.config.idfDepartments.includes(departmentCode);
const table = isIDF ? this.config.idfTable : this.config.otherTable;
const row = this.getRow(table, numPeople);
if (annualIncome <= row.blue) return "blue";
if (annualIncome <= row.yellow) return "yellow";
if (annualIncome <= row.violet) return "violet";
return "pink";
}
}
Le fichier de configuration JSON est directement transcrit du tableur. Quand les seuils changent, on met à jour le JSON, on met à jour les tests, et c’est terminé. Le client peut même mettre à jour le JSON lui-même si besoin — c’est du JSON, pas du code.
Ce pattern — données dans un fichier de configuration, logique dans un service testé — s’est révélé être le plus maintenable pour les règles métier qui changent fréquemment. Le code ne change pas quand les barèmes évoluent. Seules les données changent.
Kysely — le choix qui a bien vieilli
On avait choisi Kysely au démarrage parce que c’était le query builder recommandé dans le template Vercel Postgres — le package @vercel/postgres-kysely était la voie de moindre résistance pour démarrer vite. Je ne connaissais pas Kysely avant ce projet. Je connaissais Knex, Drizzle, et évidemment Prisma — mais Kysely était nouveau pour moi.
Ce qui m’a convaincu de rester, c’est le typage. En bon geek TypeScript, pouvoir typer le schéma de base de données à la main et avoir le compilateur qui vérifie chaque requête à la compilation, ça m’a vendu le truc. Pas de client généré, pas de fichier de schéma à synchroniser — on définit les types des tables dans un fichier TypeScript, et Kysely fait le reste.
interface Database {
User: {
id: string;
email: string;
hashedPassword: string;
role: "AGENT" | "ARAI" | "DIRECTOR";
};
Property: {
id: string;
userId: string;
status: PropertyStatus;
completedSteps: number;
surface: number | null;
// ...
};
}
const db = new Kysely<Database>({ dialect: postgresDialect });
Après dix-huit mois et trente-cinq migrations, ce système tient toujours. Chaque migration met à jour les types manuellement — c’est un peu de travail, mais c’est aussi une documentation vivante du schéma. On sait exactement ce que contient chaque table en lisant un fichier TypeScript.
Le seul bémol : Vercel a commencé à migrer son offre Postgres vers des intégrations Neon natives fin 2024. Le package @vercel/postgres-kysely n’est plus la voie recommandée. On continue de l’utiliser sans problème, mais si on démarrait aujourd’hui, on connecterait probablement Kysely directement à Neon via leur driver serverless.
Ce qu’on ferait différemment
Le polling pour les traitements IA. Le polling HTTP toutes les deux secondes fonctionne, mais ce n’est pas élégant. Avec le recul, des Server-Sent Events auraient été plus propres — un flux continu côté serveur qui notifie le client à chaque changement d’état. Next.js supporte les ReadableStreams dans les Route Handlers, on aurait pu l’implémenter sans infrastructure supplémentaire.
La gestion des migrations Kysely. Le script de migration custom fonctionne, mais on aurait dû investir plus tôt dans un outil comme kysely-ctl pour automatiser la génération des fichiers de migration et la synchronisation avec les types.
Les tests d’intégration des Server Actions. On a bien testé les services métier, mais les Server Actions elles-mêmes ne sont couvertes que par les tests end-to-end de la CI. Des tests d’intégration plus ciblés — appeler la Server Action directement avec des données de test et vérifier l’état de la base — nous auraient fait gagner du temps lors de la montée Next.js 15.
Le bilan
Le TDD sur les formules Excel a été le meilleur investissement du projet. Deux jours de travail au démarrage, des dizaines d’heures économisées sur les changements de barèmes et les montées de version. Si on ne devait retenir qu’une chose de ces dix-huit mois, c’est ça : testez la logique métier, pas le framework.
La coordination de deux IAs (Document AI + GPT-4) fonctionne en production depuis plus d’un an. Ce n’est pas magique — il faut gérer les erreurs, les hallucinations, les timeouts — mais le gain de productivité pour les agents est réel : un formulaire qui se pré-remplit en trente secondes au lieu de vingt minutes de saisie manuelle.
Et Next.js ? Le framework a évolué vite — trop vite, diront certains — mais les fondations qu’on a posées avec les Server Actions et l’App Router en décembre 2023 tiennent toujours. Le code métier n’a pas changé. C’est la plomberie qui bouge, et c’est normal. Les tests nous protègent. C’est à ça qu’ils servent.
Articles précédents dans cette série : l’architecture initiale Next.js 14 et les Server Actions en production après six mois.