# Server Actions en production : formulaires multi-étapes et uploads avec Next.js 14 > Retour d'expérience sur l'utilisation des Server Actions en production depuis six mois. Formulaires multi-étapes, uploads de fichiers avec Vercel Blob, validation Zod, et sauvegarde progressive — ce que Next.js 14 change concrètement pour les applications métier. Date : 20/06/2024 Auteur : Aurélien N. Tags : Next.js, Server Actions, TypeScript, Vercel, React --- Ça fait six mois qu'on utilise les Server Actions de Next.js 14 en production sur [Réno Claire](/blog/next-14-app-router-simulation-renovation-energetique), un simulateur de rénovation énergétique. Pas sur un side project, pas sur un proof of concept — en production, avec de vrais agents immobiliers qui saisissent des données toute la journée depuis leurs tablettes. Le projet repose sur des formulaires multi-étapes complexes : cinq à six écrans par simulation, avec des champs conditionnels, des uploads de fichiers, des calculs intermédiaires, et une sauvegarde progressive en base. C'est le type d'application où les Server Actions brillent — ou exposent leurs limites. Après six mois, on a une vision claire des deux. ## Le pattern multi-étapes L'architecture est simple et elle n'a pas bougé depuis le premier jour : chaque étape est une page Server Component à une URL distincte. ``` /form/s0/[formId]/step/1 → propriétaire, revenus /form/s0/[formId]/step/2 → couleur foyer, adresse /form/s0/[formId]/step/3 → surface, énergie, photos /form/s0/[formId]/step/4 → enveloppe, niveaux /form/s0/[formId]/step/5 → détails, toiture, vitrage ``` L'URL **est** l'état. Pas de state management, pas de contexte React, pas de store Zustand. Quand un agent soumet l'étape 3, la Server Action sauvegarde en base et appelle `redirect("/form/s0/$/step/4")`. Next.js rend la page 4 côté serveur, charge les données depuis Postgres, et envoie le HTML au navigateur. L'agent peut fermer son onglet, revenir trois jours plus tard, taper l'URL de l'étape 4 — tout est là. On a résisté à la tentation d'un formulaire wizard côté client. C'est tentant sur le papier — transitions fluides, pas de rechargement — mais dans la pratique : - Les agents perdent régulièrement leur connexion en déplacement - Certains laissent un onglet ouvert pendant des heures - Le back/forward du navigateur doit fonctionner naturellement - Un crash de navigateur ne doit pas perdre de données Avec des pages serveur, tous ces cas sont gérés par défaut. La base est la source de vérité, pas le DOM. ## Server Actions — ce qui marche bien ### La colocalisation Le gain le plus immédiat, c'est la lisibilité. L'action est à côté du formulaire qui l'utilise. Pas de fichier `/api/form/step-1/route.ts` à aller chercher dans un autre dossier. Le développeur qui lit le code voit le formulaire, remonte de trois lignes, et trouve la logique serveur. ```typescript // step/3/action.ts "use server"; const userId = await authorizeUserOrThrow([ "AGENT", "ARAI", "DIRECTOR" ]); const data = step3Schema.parse(); await db.updateTable("PropertyS0") .set() .where("id", "=", formId) .where("userId", "=", userId) .execute(); redirect(`/form/s0/$/step/4`); } ``` Autorisation, validation, persistence, navigation — tout tient dans une fonction de vingt lignes. Et cette fonction est **typée** de bout en bout grâce à Zod et Kysely. ### Le progressive enhancement Les Server Actions utilisent un POST natif du navigateur. Si JavaScript n'est pas encore chargé (ou s'il plante), le formulaire fonctionne quand même. Sur une tablette en 4G avec un bundle qui met trois secondes à s'hydrater, l'agent peut déjà soumettre. React enhancera le formulaire quand il sera prêt — pending states, optimistic updates — mais la fonctionnalité de base est là immédiatement. On a mesuré : sur les tablettes Android bas de gamme utilisées par certains agents, le Time to Interactive est de 2,8 secondes. Sans progressive enhancement, les formulaires seraient inutilisables pendant ces trois secondes. Avec les Server Actions, ils sont fonctionnels dès le premier rendu HTML. ### La validation serveur-first On valide tout côté serveur, point. Zod parse les données, et si ça échoue, on retourne les erreurs au formulaire. Pas de validation côté client en doublon — on laisse le navigateur faire les validations HTML5 de base (required, type="email"), et le serveur fait le reste. ```typescript const step1Schema = z.object(); ``` Le schéma Zod sert à la fois de validation et de documentation. Un nouveau développeur lit le schéma et comprend exactement ce que chaque étape attend — types, contraintes, messages d'erreur. ## Les uploads de fichiers C'est le cas d'usage où les Server Actions ont montré leurs limites — et où Vercel Blob a pris le relais. Pour l'upload du DPE (un scan PDF qui peut peser plusieurs mégaoctets), on ne peut pas passer par une Server Action classique. La taille maximale du body d'une Server Action est limitée, et on veut un upload progressif avec indicateur de progression — ce qu'un POST classique ne fournit pas. On utilise Vercel Blob avec son système de **client uploads** : ```typescript // API route pour générer un token d'upload const body = await request.json(); const = body; // Vérification d'autorisation const userId = await authorizeUser(["AGENT", "ARAI", "DIRECTOR"]); if (!userId) return new Response("Unauthorized", ); return handleUpload({ request, body, onBeforeGenerateToken: async () => ({ allowedContentTypes: ["application/pdf", "image/jpeg", "image/png"], tokenPayload: JSON.stringify(), }), onUploadCompleted: async () => { const = JSON.parse(tokenPayload); await db.updateTable("Dpe") .set() .where("propertyId", "=", propertyId) .execute(); }, }); } ``` Le flow est en trois temps : 1. Le client demande un token d'upload (via API route, pas Server Action) 2. Le client upload directement vers Vercel Blob avec ce token — progress bar, retry, tout ce qu'il faut 3. Vercel Blob envoie un webhook à notre `onUploadCompleted` quand c'est terminé Côté UI, un composant `FileLoaderProgress` affiche la progression et le statut. Le composant poll le statut du DPE en base pour savoir quand l'OCR et l'analyse IA sont terminées. ## Les états intermédiaires Un pattern qu'on a développé pour gérer les traitements longs : le **status machine** en base de données. Le DPE passe par six états : ``` uploading → uploaded → ocr-analyzing → ocr-analyzed → ai-analyzing → ai-analyzed ↘ error ``` Chaque transition est atomique. Le service qui lance l'OCR met le statut à `ocr-analyzing` avant d'appeler Google Document AI, puis à `ocr-analyzed` quand le résultat arrive. Même chose pour l'extraction GPT-4. Le composant client poll le statut toutes les deux secondes et affiche un indicateur visuel adapté : spinner pendant l'OCR, animation pendant l'analyse IA, résultat ou erreur à la fin. Si quelque chose échoue, le statut passe à `error` et l'agent peut saisir les données manuellement. Ce pattern — état machine en base + polling côté client — est plus robuste qu'un WebSocket pour notre cas d'usage. Les traitements prennent entre 10 et 40 secondes. Un WebSocket serait overkill et ajouterait de la complexité (reconnexion, heartbeat, infrastructure). Un polling toutes les deux secondes, c'est simple, fiable, et ça fonctionne même si l'agent rafraîchit la page. ## Next.js 14.1 et 14.2 — les améliorations en chemin En janvier, Next.js 14.1 a corrigé une vingtaine de bugs sur les routes parallèles et interceptées, et amélioré les messages d'erreur — deux points qui nous impactaient directement. En avril, Next.js 14.2 a marqué un cap avec Turbopack en Release Candidate (99,8 % des tests passent). On ne l'utilise pas encore en production, mais le `next dev --turbo` en local est devenu notre défaut : le serveur démarre en moins d'une seconde au lieu de quatre. La version 14.2 a aussi introduit les `staleTimes` configurables pour le cache client — un sujet sur lequel on avait des frictions. Par défaut, Next.js cache agressivement les navigations client. Pour une application métier où les données changent fréquemment, c'est un problème : un agent qui soumet l'étape 3, revient en arrière, et retourne à l'étape 4 pouvait voir des données stales. Les `staleTimes` nous ont permis de réduire la durée du cache client à zéro pour nos routes dynamiques. ## Les leçons après six mois **Ce qu'on referait sans hésiter :** - Server Actions pour tous les formulaires. La simplification du code est massive. Pas de layer API intermédiaire, pas de client fetch, pas de synchronisation de types. - URL comme état. Le modèle multi-pages est naturel pour les utilisateurs, robuste pour la persistance, et simple à implémenter. - Validation Zod côté serveur uniquement. Pas de duplication de logique de validation entre client et serveur. - Kysely plutôt qu'un ORM. C'était le choix par défaut du template Vercel Postgres, je ne connaissais pas avant — mais le typage end-to-end à la main est devenu un vrai plaisir de geek TypeScript. Sur un projet avec du SQL complexe, le query builder typé est imbattable. **Ce qu'on adapterait :** - Les uploads devraient passer par une API route dédiée dès le départ, pas par une Server Action. On a perdu du temps à essayer de faire rentrer les uploads dans le modèle Server Action avant de comprendre que ce n'était pas le bon outil. - Le polling pour les traitements longs fonctionne, mais on envisage Server-Sent Events pour les prochaines itérations — moins de requêtes, feedback plus immédiat. **Ce qui reste fragile :** - NextAuth v5 est toujours en beta. On n'a pas eu de bug bloquant, mais les breaking changes entre les versions beta sont fréquents. Chaque mise à jour de la dépendance demande une relecture du changelog. - Le cold start des fonctions Vercel en plan Pro est perceptible sur les premières requêtes après une période d'inactivité. Pour une application métier utilisée en heures ouvrées, c'est acceptable. Pour un SaaS grand public, ce serait un problème. ## Six mois de Server Actions, et alors ? Le paradigme Server Actions tient ses promesses sur un cas d'usage réel : une application métier avec des formulaires complexes, de la persistance, et des traitements asynchrones. Le code est plus simple, plus lisible, et plus facile à maintenir qu'une architecture API traditionnelle. Ce n'est pas magique — les uploads, les traitements longs et le cache client demandent toujours de la réflexion. Mais le ratio signal/bruit est nettement meilleur qu'avant. On écrit moins de code, et ce code fait ce qu'il dit. La prochaine étape pour nous : la montée vers Next.js 15 quand il sortira, et l'adaptation aux changements de comportement du cache qui s'annoncent. Les tests qu'on a écrits depuis le premier jour devraient nous protéger — c'est la théorie, en tout cas. On verra en pratique. --- *Article précédent : [l'architecture initiale de Réno Claire](/blog/next-14-app-router-simulation-renovation-energetique). Article suivant : [le retex à 18 mois — TDD, montée de version et coordination d'IAs](/blog/retex-tdd-montee-version-coordination-ia-renovation).*