Files
_Assistant_Lead_Tech/knowledge/backend/patterns/contracts.md
2026-03-31 15:39:20 +02:00

6.0 KiB

title: Backend — Patterns : Contracts domain: backend bucket: patterns tags: [contracts, zod, api, error-codes, monorepo] applies_to: [analysis, implementation, review, architecture] severity: high validated_on: 2026-03-20 source_projects: [app-alexandrie]

Backend — Patterns : Contracts

Extrait de la base de connaissance Lead_tech. Voir knowledge/backend/patterns/README.md pour l'index complet.


Pattern : Contracts-First / Zod-Infer / No-DTO (monorepo TypeScript fullstack)

  • Objectif : avoir une seule source de vérité pour les contrats d'interface entre API et client, sans redéfinition manuelle de types.
  • Contexte : monorepo TypeScript avec un package partagé (packages/contracts ou équivalent), consommé par le backend et le front/mobile.
  • Quand l'utiliser : dès qu'une API est consommée par un client TypeScript dans le même repo.
  • Quand l'éviter : si le client est externe (autre organisation, autre langage) — dans ce cas, OpenAPI reste la référence.
  • Avantage :
    • Zéro drift entre contrat et implémentation
    • Types TypeScript gratuits via z.infer<> — aucune réécriture
    • Changement de contrat = erreur de compilation immédiate côté client
    • Mocks de tests alignés automatiquement
  • Limites / vigilance :
    • Ne pas mettre de logique métier dans packages/contracts (IO only)
    • Attention aux dépendances circulaires si le package grossit
  • Validé le : 07-03-2026
  • Contexte technique : TypeScript / Zod / NestJS + Expo (React Native) — pattern agnostique framework

Implémentation (exemple minimal)

// packages/contracts/src/auth/auth.schemas.ts
export const RegisterRequestSchema = z.object({
  email: z.string().email(),
  password: z.string().min(8),
});
export type RegisterRequest = z.infer<typeof RegisterRequestSchema>; // type GRATUIT

// packages/contracts/src/index.ts
export * from './auth/auth.schemas';
export * from './errors/error-code';

// apps/api/src/modules/auth/auth.controller.ts
import type { RegisterRequest } from '@monrepo/contracts';
// + ZodValidationPipe → validation automatique, zéro DTO manuel

// apps/mobile/src/domains/auth/auth.store.ts
import type { RegisterRequest } from '@monrepo/contracts';
// même type, même schéma, zéro duplication

Structure cible du package contracts

packages/contracts/src/
  auth/auth.schemas.ts       ← request/response auth
  users/users.schemas.ts     ← request/response users
  billing/billing.schemas.ts ← request/response billing (Epic suivant)
  errors/error-code.ts       ← enum codes d'erreur stables
  http/envelopes.ts          ← { data, meta } / { error, meta }
  index.ts                   ← re-export tout

Ce qui appartient à contracts

  • Schémas Zod request/response
  • Types inférés (z.infer<>)
  • Codes d'erreur applicatifs stables
  • Enums et constantes partagées (ex : liste officielle de sujets/topics)

Ce qui n'appartient PAS à contracts

  • Logique métier
  • Modules/services/guards framework (NestJS, etc.)
  • State management client (Zustand, Redux, etc.)

Checklist

  • Zéro DTO manuel dans l'API — uniquement z.infer<typeof Schema>
  • ZodValidationPipe global ou par endpoint pour la validation d'entrée
  • Constantes partagées (enums, listes) dans contracts, jamais dupliquées
  • Mocks de tests importent les types depuis contracts

Pattern : Contracts-First — error codes comme contrat obligatoire

  • Objectif : maintenir les codes d'erreur API dans packages/contracts pour éviter les clients stringly-typed.
  • Contexte : monorepo TypeScript avec packages/contracts/src/errors/error-code.ts.
  • Règle : toute nouvelle erreur API ⇒ ajout obligatoire dans error-code.ts avant merge, pas après.
  • Risque si ignoré : clients qui testent des strings hardcodées au lieu d'importer l'enum → drift silencieux.
  • Validé le : 09-03-2026
  • Contexte technique : TypeScript / NestJS + Expo (React Native)

Checklist

  • Nouvel error.code → ajout dans packages/contracts/src/errors/error-code.ts en même commit
  • Clients importent l'enum, pas une string littérale
  • PR review : vérifier error-code.ts à chaque ajout d'endpoint d'erreur

Pattern : Réponse HTTP 200 avec payload métier pour les états d'accès

  • Objectif : éviter les codes 4xx pour des états métier normaux qui nécessitent un rendu côté client.
  • Contexte : endpoints dont la réponse varie selon les droits ou l'état d'abonnement, sans que l'absence de contenu soit une erreur.
  • Quand l'utiliser : paywall, trial read-only, quota soft, état d'accès partiel — quand le client doit décider du rendu.
  • Quand l'éviter : accès réellement interdit côté serveur (403), non authentifié (401), endpoint inexistant (404).
  • Avantage :
    • pas de gestion d'exception côté client mobile pour des états courants
    • rendu conditionnel (paywall, teaser, empty) piloté par le payload
    • log serveur propre — 4xx réservés aux erreurs techniques/sécurité
  • Limites / vigilance :
    • ne pas généraliser aux vraies erreurs de sécurité — 401/403/404 gardent leur sémantique HTTP
  • Validé le : 20-03-2026
  • Contexte technique : NestJS / Expo React Native — app-alexandrie story 4.1

Implémentation (exemple minimal)

// GET /community/forums
// Sans abonnement → 200 + { data: { forums: [], paywallRequired: true }, meta }
// Avec abonnement → 200 + { data: { forums: [...], paywallRequired: false }, meta }

// ❌ Anti-pattern
return res.status(402).json({ error: { code: 'SUBSCRIPTION_REQUIRED' } });

// ✅ Pattern correct
return res.status(200).json({
  data: { forums: [], paywallRequired: true },
  meta: { total: 0 },
});

Règle

  • 4xx = erreur technique ou de sécurité (401 non authentifié, 403 accès interdit, 404 introuvable)
  • 200 + flag métier = état métier normal que le client doit interpréter pour le rendu