# Backend — Patterns : Next.js > Extrait de la base de connaissance Lead_tech. Voir `knowledge/backend/patterns/README.md` pour l'index complet. --- ## Pattern : Next.js runtime-only — orchestration en bord et logique pure testable - Objectif : préserver la testabilité unitaire et la lisibilité du code serveur Next.js en limitant les dépendances runtime-only aux couches d'orchestration. - Contexte : applications Next.js avec Server Actions, route handlers, modules email/auth et logique métier testée côté Node. - Quand l'utiliser : dès qu'un flux serveur mélange APIs Next.js runtime-only (`cookies()`, `headers()`, `redirect()`, `server-only`) et logique métier réutilisable. - Quand l'éviter : petits modules purement runtime sans logique métier notable, ou fonctions triviales sans intérêt à être testées séparément. - Avantage : - garde la logique métier importable dans un runner Node standard - évite que `server-only` contamine des modules purs - facilite les tests unitaires sans mocks lourds du runtime Next.js - clarifie la responsabilité des Server Actions et handlers serveur - Limites / vigilance : - demande une discipline de découpage - peut introduire une indirection inutile si la logique extraite est réellement triviale - les frontières d'injection doivent rester simples pour éviter un excès d'abstraction - Validé le : 19-03-2026 - Contexte technique : Next.js / Server Actions / Node test runner / modules backend injectables ### Implémentation (exemple minimal) ```txt - réserver `import "server-only"` aux fichiers qui utilisent réellement des APIs runtime Next.js - garder la Server Action, route handler ou module email comme couche d'orchestration fine - extraire la logique métier pure dans une fonction ou un service sans dépendance à `cookies()`, `headers()`, `redirect()` ou `server-only` - injecter explicitement les dépendances utiles (client DB, token, callback de redirect, logger, etc.) - tester unitairement le module pur dans le runner Node ; tester l'orchestrateur plus légèrement ``` ### Checklist - `server-only` absent des modules de logique pure - APIs Next.js runtime-only limitées aux couches d'entrée - Logique métier principale testable sans runtime Next.js - Dépendances injectées explicitement quand utile - Server Action ou handler fin et lisible --- ## Pattern : Next.js server-only & Server Actions — règles d'isolation - Objectif : permettre les tests unitaires Node tout en gardant les contraintes runtime Next.js là où elles sont nécessaires. - Contexte : monorepo Next.js App Router avec logique métier testée en Node runner natif. - Quand l'utiliser : dès qu'un module mixe logique pure et dépendances runtime Next.js. - Quand l'éviter : modules purement UI côté client. - Avantage : - logique pure testable sans friction (runner Node natif) - Server Action fine et lisible — orchestration uniquement - `server-only` explicite et intentionnel, pas par habitude - Limites / vigilance : - ne pas mettre `server-only` dans les repositories purs — casse les tests Node hors Next.js - Validé le : 16-03-2026 - Contexte technique : Next.js App Router / Node.js test runner ### Règles ```txt - `server-only` uniquement sur les modules qui appellent des APIs Next.js runtime (cookies(), headers(), redirect()) — pas sur les repositories ni la logique pure - Logique pure extraite dans un module injectable sans `server-only` : deleteSession({ prismaClient, sessionToken }) → testable avec le runner Node sans friction - Server Action = orchestration mince, elle appelle les modules purs injectés et gère les dépendances Next.js runtime uniquement - Logique de validation / sanitisation (safeHttpUrl, etc.) → module utilitaire séparé, sans import nodemailer / server-only ``` ### Checklist - [ ] `server-only` absent des repositories et modules de logique pure - [ ] Server Action ≤ 10 lignes, délègue au module pur injectable - [ ] Modules purs couverts par des tests `.spec.ts` Node sans config spéciale - [ ] La logique pure ne dépend pas du runtime pour être exécutée --- ## Pattern : Utilitaires purs — extraire dans un module sans `server-only` - Objectif : permettre aux repositories et aux tests d'importer la même implémentation des utilitaires purs sans friction. - Contexte : fonctions pures (slugify, formatters, validators) utilisées par des repositories qui ont `server-only`. - Quand l'utiliser : dès qu'une fonction pure est utilisée dans un repository ET dans des tests. - Risque si ignoré : logique dupliquée dans les tests qui diverge silencieusement de l'implémentation réelle. - Validé le : 21-03-2026 - Contexte technique : Node.js / Next.js — app-template-resto ### Implémentation ``` src/server/menuAdmin/ allergensRepository.ts ← import { slugify } from "./slugify" slugify.ts ← export function slugify() {} // pas de "server-only" tests/ allergens-admin.test.ts ← import { slugify } from "../src/server/menuAdmin/slugify.ts" ``` --- ## Pattern : Réutiliser un champ existant plutôt que créer un modèle dédié en V1 - Objectif : éviter la sur-ingénierie en V1 en réutilisant un champ existant quand le besoin est simple. - Contexte : early-stage, besoin de stocker une configuration simple (URL, flag, valeur unique). - Quand l'utiliser : quand la donnée a le même cycle de vie qu'un modèle existant et ne nécessite pas de relations. - Quand l'éviter : si la configuration a son propre cycle de vie, des cardinalités multiples, ou des relations distinctes. - Avantage : zéro migration supplémentaire, zéro scope creep - Validé le : 17-03-2026 - Contexte technique : Prisma / Node.js — app-template-resto ### Règle ```txt - Avant de créer un modèle ReservationConfig, vérifier si PublicHomeProfile.reservationUrl suffit - Un champ optionnel dans le modèle le plus proche est suffisant en V1 - Ne créer un modèle dédié que si : cycle de vie distinct, relations, ou cardinalités multiples ``` --- ## Pattern : Valider le protocole d'une URL externe avant de la passer à un lien public - Objectif : prévenir les injections `javascript:` et URLs malformées dans les `` ou `` publics. - Contexte : toute URL venant d'une config tenant, DB ou saisie utilisateur, rendue dans le HTML. - Quand l'utiliser : systématiquement sur tout champ URL libre stocké en DB et rendu côté HTML. - Risque si ignoré : injection `javascript:`, URL malformée, potentiel XSS. - Validé le : 17-03-2026 - Contexte technique : Node.js / Next.js — app-template-resto ### Implémentation ```typescript function isSafeUrl(url: string): boolean { try { const { protocol } = new URL(url); return protocol === "https:" || protocol === "http:"; } catch { return false; } } // Validation complète en service/repository if (mediaUrl) { try { new URL(mediaUrl); } catch { throw new HttpError("URL invalide.", { status: 400 }); } if (!mediaUrl.startsWith("https://") && !mediaUrl.startsWith("http://")) throw new HttpError("URL doit commencer par https://.", { status: 400 }); if (mediaUrl.length > 500) throw new HttpError("URL trop longue.", { status: 400 }); } // Retourner null si invalide — le composant gère l'absence d'URL ``` ### Checklist - [ ] Validation format (`new URL()`) + protocole + longueur max - [ ] Retourner `null` si invalide, jamais passer la string brute - [ ] Composant UI reçoit `string | null`, jamais une string non vérifiée --- ## Pattern : Next.js `after()` pour fanout post-réponse - Objectif : exécuter du travail accessoire (notif push, audit, webhook) APRÈS avoir renvoyé la réponse HTTP, sans bloquer la latence métier ni risquer la fermeture lambda du fire-and-forget naïf. - Contexte : route Next.js 15+ (App Router) qui termine une mutation métier (ex : `prisma.$transaction`) et veut déclencher un effet de bord non critique. - Quand l'utiliser : effet de bord best-effort qui ne doit JAMAIS bloquer la réponse principale (push, log audit asynchrone, webhook sortant). - Quand l'éviter : effet de bord qui DOIT réussir avant de répondre 200 (validation transactionnelle, écriture critique). - Avantage : - réponse HTTP immédiate (latence métier préservée) - le runtime Node attend la fin du callback avant de fermer le process — pas de fire-and-forget orphelin - les exceptions du callback sont attrapables localement, ne propagent pas au caller - Limites / vigilance : - **JAMAIS placer `after()` à l'intérieur d'une `prisma.$transaction(async tx => { ... after(...) })`** : si la transaction rollback, le callback `after()` reste planifié et s'exécute sur des données qui n'existent pas en DB - construire le payload SYNCHRONE avant `after()` et attraper les erreurs là, sinon une exception dans le callback async devient silencieuse - comportement en serverless (Vercel, Lambda) vs Node standalone peut différer — tester en cible - Validé le : 28-04-2026 - Contexte technique : Next.js 15+ App Router — RL799_V2 ### Implémentation ```typescript import { after } from 'next/server'; export const notifyConvocationPublished = async ( tenueId: string, recipientIds: string[], ): Promise => { // 1. Mutation métier (transactionnelle) await prisma.$transaction(async (tx) => { await tx.notification.createMany({ data: ... }); }); // 2. Construction payload SYNCHRONE — toute exception attrapée ici let payload: PushPayload; try { payload = buildPushPayload(tenueId); } catch (err) { log.warn('payload build failed', { err }); return; } // 3. Hook après la transaction — JAMAIS dedans after(async () => { try { await pushService.sendPushToUsers(recipientIds, payload); } catch (err) { log.warn('fanout failed', { err }); // jamais throw vers le caller } }); }; ``` ### Anti-patterns - ❌ `await pushService.sendPushToUsers(...)` dans le service métier (bloque la latence + propage les erreurs) - ❌ `after(() => { ... })` à l'intérieur d'une `prisma.$transaction(async tx => { ... after(...); })` - ❌ Construire le payload DANS le callback `after()` async — une exception y devient silencieuse ### Checklist - [ ] `after()` appelé APRÈS le `await prisma.$transaction(...)`, jamais à l'intérieur - [ ] Payload construit synchrone avant `after()`, exceptions attrapées localement - [ ] Le callback `after()` n'a pas le droit de throw (best-effort wrapping) - [ ] La route métier renvoie 2xx même si le fanout échoue --- ## Pattern : Gate "agir au nom de X" (3 étages : rôle → type/scope → cible actif) - Objectif : valider correctement une autorisation d'override d'attribution (`uploadedByOverride`, `createdByOverride`) pour qu'un rôle élevé puisse agir "au nom de" un autre user, sans laisser de faille. - Contexte : endpoint API qui accepte un override d'attribution (archiviste upload pour un Frère, admin crée une entité pour X). - Quand l'utiliser : tout endpoint avec un override d'attribution sensible. - Quand l'éviter : si la délégation est implicite et déjà couverte par un guard centralisé. - Avantage : - validation strictement séquentielle et défensive — chaque étage a son code HTTP propre - le check "actif" combine toutes les dimensions disponibles (pas un seul flag) - Limites / vigilance : - ordre impératif : rôle EN PREMIER (plus sensible). Un membre lambda envoyant un payload avec override doit recevoir 403 même si la cible est valide - Validé le : 20-04-2026 - Contexte technique : Next.js / API HTTP — RL799_V2 ### Les 3 étages 1. **Rôle** : le user courant a-t-il la capacité ? Sinon **403 FORBIDDEN**. 2. **Type/scope** : l'override est-il pertinent pour ce type d'entité ? Sinon **400 VALIDATION_ERROR**. 3. **Cible** : la cible existe-t-elle ET est-elle active ? Sinon **400 VALIDATION_ERROR** si introuvable OU inactive OU démissionnée OU décédée. ### Implémentation ```typescript let effectiveUploadedBy = currentUser.id; if (metadata.uploadedByOverride) { // Étage 1 : rôle if (userRole !== 'archiviste' && userRole !== 'admin') { return errorResponse(403, 'FORBIDDEN', '...'); } // Étage 2 : type/scope if (metadata.type !== 'planche') { return errorResponse(400, 'VALIDATION_ERROR', '...'); } // Étage 3 : cible actif (toutes les dimensions disponibles) const targetUser = await getUserById(metadata.uploadedByOverride); const isInactive = !targetUser || !targetUser.isActive || !!targetUser.profile?.resignedAt || !!targetUser.profile?.deceasedAt; if (isInactive) { return errorResponse(400, 'VALIDATION_ERROR', '...'); } effectiveUploadedBy = metadata.uploadedByOverride; } ``` ### Pourquoi vérifier toutes les dimensions Un user peut être `isActive: true` dans le système mais avoir une `resignedAt` antérieure (désactivation non-synchrone). Le check "actif" doit combiner **toutes** les dimensions disponibles du modèle, pas un seul flag.