Aller au contenu

Validation — zod, et la sortie d'idonttrustlikethat

La décision

Tout le nouveau code valide et type avec zod (v4). idonttrustlikethat (idtlt) est gelé : il reste dans le legacy et les contrats existants, mais n'entre dans aucun nouveau module.

Le contexte

idtlt avait été introduit pour son API pratique, mais le projet n'est plus maintenu. zod est le standard éprouvé de l'écosystème TypeScript et continue de s'améliorer en continu — même schéma de décision que pour space-lift (Erreurs & Result) : on ne reste pas sur une lib morte, aussi bonne ait été son API.

Pourquoi zod

  • Source unique des types : z.infer<typeof schema> — le schema EST le type, zéro duplication. Même rôle que validator.T d'idtlt, mais sur un socle vivant.
  • Branded types natifs : z.uuid().brand<'ProjectFinancingRequestId'>() — les IDs forts au cœur de notre façon de coder, propagés jusqu'à Kysely.
  • Écosystème : z.toJSONSchema() génère l'OpenAPI des endpoints via le décorateur @ApiZodEndpoint — la doc Swagger sort des schemas, sans annotations à maintenir. Le tooling moderne (MCP, agents, libs tierces) parle zod nativement.
  • Les unions discriminées portent nos états d'entités (pattern JSONB) : un état = un schema, et une transition peut se valider en parsant le schema cible.

Tout valide aux frontières

Tout ce qui entre dans le système est validé, et chaque frontière a son helper (safe-parse.zod.ts) :

Frontière Helper Erreur
HTTP body/query bodySafeParse_zod(schema, body) validation-body (400)
Rows DB ky_parseRows / ky_parseOneRow / pgSafeParse_zod validation-pg (500)
APIs externes validation de la réponse dans le provider code dédié (ex. pappers-search-failed)

Pas de pipe NestJS global ni de nestjs-zod : la validation est explicite dans chaque controller. C'est cohérent avec le reste de l'architecture — pas de magie, pas de décorateur porteur de comportement caché — et le Result retourné s'enchaîne naturellement avec throwApiError.

Les contrats front ↔ back vivent dans les packages api-communication-* — ex. api-communication-project-owner : schemas zod partagés, le front et l'API valident exactement la même chose.

La politique de transition

  • Nouveaux modules : full zod. Aucun import idtlt dans la stack cible (rule explicite).
  • Jamais de rétro-migration : les contrats idtlt existants (api-communication, api-communication-bricksoffice…) restent en l'état tant que l'endpoint vit.
  • Cohabitation au bord : les routes internal-* illustrent la doctrine — contrats zod côté API exposée, lecture des tables legacy via queryPgAndValidate + idtlt côté données. zod gagne du terrain par les bords ; le centre suit quand on migre le module.

Limites assumées

  • Deux libs de validation cohabitent durablement — coût accepté de la migration par attrition.
  • Les brands ne survivent pas à la sérialisation JSON d'un contrat : sur ces champs, .describe('… en centimes') documente l'unité (naming).