Détection de la dérive des contrats API silencieuse à l’aide de types inférés à partir d’échantillons et de Zod
Pourquoi les types TypeScript manuscrits pour des API tierces deviennent obsolètes, comment l’inférence de types et les schémas Zod à partir des réponses réelles aide, et comment les différences de snapshots révèlent les écarts.
Les API de tiers changent de forme sans avertissement, et TypeScript ne s’en rendra pas compte, car vos types décrivent à quoi ressemblait la réponse au moment où elle a été écrite, et non à quoi elle ressemble aujourd’hui. Cet article explique comment cette faille se produit, pourquoi générer des types à partir de plusieurs réponses réelles est préférable à les taper manuellement, et pourquoi c’est en comparant de nouvelles réponses avec un snapshot sauvegardé que l’on parvient véritablement à se protéger. Vous verrez également où cette approche s’inscrit par rapport aux tests OpenAPI et contractuels, et où elle ne convient pas.
Comment un champ rebaptisé peut passer inaperçu
Considérez une interface utilisateur qui s’intègre à un prestataire de paiement. Un jour, un champ dans l’une des réponses du prestataire passe de user_id à userId. Il n’y a ni entrée dans le journal des modifications, ni annonce, ni mise à jour de version. Il est très probable qu’un ingénieur du côté du prestataire ait corrigé une incohérence de nom, que leur suite de tests ait fonctionné, et que la modification ait été déployée.
Rien ne plante du côté de l’application cliente, ce qui est justement le problème. Le code lit toujours response.user_id, et TypeScript l’accepte, car l’interface a été définie manuellement il y a des mois à partir d’un exemple de Postman qui ne reflète plus la réalité. Cette interface promet toujours un champ user_id. Au moment de l’exécution, la valeur est simplement undefined. Pendant deux semaines, trois chemins de code écrivent discrètement undefined dans un champ de montant, jusqu’à ce qu’un ticket de support révèle finalement la faille. Aucun avertissement n’est déclenché, aucune compilation ne se bloque, et l’application continue de faire ce qui est incorrect sans aucun problème.
Les équipes qui travaillent avec des API externes pendant une période suffisamment longue rencontrent presque toujours une version de ce type d’incident.
Le point faible réside dans l’origine des types
TypeScript n’est pas en cause ici. Le problème vient de l’origine des types. Les interfaces sont généralement considérées comme issues d’une source autorisée, telle qu’un schéma, un contrat ou une source unique de vérité. En pratique, beaucoup d’entre elles proviennent d’une réponse d’exemple que quelqu’un a transcrite à la main. Cette interface est ensuite copiée dans plusieurs autres fichiers et traitée comme une vérité absolue, sans que personne ne la revoie avant qu’un problème ne survienne.
Le véritable contrat est ce que l’API renvoie actuellement en production. Il se trouve sur un serveur que vous ne contrôlez pas et qui peut être modifié sans votre consentement. Les types manuscrits ne sont qu’un instantané d’un moment déjà révolu, et le compilateur n’a aucun moyen de le savoir.
Il existe également un écart plus profond. Les types de TypeScript disparaissent au moment de la compilation, ce qui signifie qu’aucune vérification n’a lieu en temps de exécution. Si la structure des données change, seule une validation en temps de exécution aux frontières, par exemple l’analyse de la réponse à l’aide d’un schéma Zod, permet de transformer une valeur undefined silencieuse en une erreur immédiate et visible.
Déduire des types à partir de plusieurs réponses réelles
Ce qui est le plus utile ici, ce n’est pas un TypeScript plus avancé ou des génériques plus ingénieux, mais plutôt un processus mécanique : prendre les réponses effectivement renvoyées par l’API, en générer des types à partir, et être averti dès que la réalité cesse de correspondre.
Les entrées doivent être des réponses JSON réelles, et non de la documentation ni un schéma. À partir d’elles, une outil peut déduire à la fois un type TypeScript et un schéma Zod équivalent. Utiliser plusieurs exemples est plus important qu’il n’y paraît. Une seule réponse montre à quoi peut ressembler un en-tête de données. Trois ou quatre réponses permettent de déterminer quels champs sont véritablement optionnels, lesquels prennent parfois la valeur null, et quels éléments d’array ont des formes incohérentes. Un seul exemple est toujours trompeur en raison d’omissions.
L’exemple ci-dessous présente deux réponses pour la même ressource, l’une avec user_id et l’autre avec userId, et montre le type TypeScript ainsi que le schéma Zod déduits des deux :
// paste these two responses in...
[
{
"user_id": "pot_00009exampleP0tOxWb",
"name": "Wedding Fund",
"balance": 550100,
"currency": "GBP",
"created": "2025-11-09T12:30:53.695Z",
"updated": "2025-02-26T07:12:04.925Z"
},
{
"userId": "pot_00009exampleP0tOxWb",
"name": "Wedding Fund",
"balance": 550,
"currency": "EUR",
"created": "2025-11-09T12:30:53.695Z",
"updated": "2025-03-26T07:12:04.925Z"
}
]
// ...get this out typescript
type Root = {
user_id?: string
name: string
balance: number
currency: string
created: string
updated: string
userId?: string
}[]
// or ... get this out zod
import { z } from 'zod'
const Root = z.array(z.object({
user_id: z.string().optional(),
name: z.string(),
balance: z.number(),
currency: z.string(),
created: z.string(),
updated: z.string(),
userId: z.string().optional(),
}))
Examinez attentivement le résultat de la fusion. Comme chaque nom n’apparaît qu dans un seul échantillon, user_id et userId deviennent tous deux optionnels. C’est techniquement exact, mais cela masque également le changement de nom : du code qui lit l’un ou l’autre champ effectue toujours une vérification de type, et une réponse ne contenant aucun des deux passerait également le schéma Zod. Les échantillons indiquent aussi un problème que l’inférence de type ne peut jamais détecter : balance passe de 550100 à 550 tandis que currency change, ce qui pourrait signifier un passage entre des unités monétaires mineures et majeures. Le type inféré est number dans les deux cas. L’inférence vous indique la forme, mais pas le sens.
De nombreux générateurs de code s’arrêtent à ce stade. Passer d’un code non typé à un code typé est utile, mais cela ne résout pas le problème de l’évolution progressive.
Les captures d’écran et les différences permettent de détecter le changement
La étape la plus importante a lieu après la génération. Une fois que des types sont dérivés à partir d’une réponse réelle, cette réponse peut être stockée sous forme de snapshot. Chaque fois que vous obtenez un nouvel échantillon depuis le même point d’entrée, vous le comparez au snapshot pour obtenir un rapport précis des modifications apportées : qu’il s’agisse d’un champ sous un nouveau nom, d’une valeur qui est désormais optionnelle alors qu’elle était auparavant une simple string, ou d’une clé supplémentaire apparaissant à l’intérieur d’un objet imbriqué. Au lieu d’une indication vague du type « quelque chose a échoué quelque part », vous voyez exactement en quoi la structure a changé.
C’est cette comparaison qui distingue un générateur de types d’un détecteur de dérive. La génération de code permet de passer de rien à du code typé. Le détecteur de dérive empêche qu’un changement de nom de user_id en userId reste inaperçu en production pendant des semaines. Dans l’exemple ci-dessus, une comparaison de snapshots indiquerait "user_id supprimé, userId ajouté" au lieu de laisser silencieusement ces deux champs devenir optionnels.
Gardez les charges de travail en production sur votre machine
Pour détecter une dérive réelle, il faut des données réelles ; des charges utiles synthétiques ne révéleront pas les changements qui vous intéressent. Cela fait de la confidentialité une exigence de conception. Les réponses produites peuvent contenir des informations sur les clients, donc les coller dans un formulaire web qui les envoie vers un serveur tiers crée un nouveau risque de traitement des données. Les outils destinés à cette tâche doivent fonctionner localement, par exemple entièrement dans l’onglet du navigateur ou en tant que script dans votre propre répertoire, afin que les charges utiles ne quittent jamais votre environnement.
Où cette approche convient et où elle ne convient pas
L’inférence basée sur des échantillons avec détection de dérive ne remplace pas OpenAPI ni un outil de test de contrat comme Pact. Si vous possédez à la fois le fournisseur et le consommateur et que vous pouvez imposer un schéma à la source, faites-le ; c’est la meilleure solution à long terme.
Cette technique vise la situation plus courante et moins reluisante : vous utilisez une API que vous ne contrôlez pas, dont la documentation est obsolète ou manquante, et il n’est pas possible de générer un client à partir d’une spécification OpenAPI car aucune spécification n’existe ou personne ne lui fait confiance. Cela décrit la plupart des intégrations avec des processeurs de paiement, des services internes gérés par d’autres équipes, ainsi que les API de fournisseurs tiers. Dans ce contexte, la réponse réelle est la seule vérité disponible, c’est donc d’elle que vos types doivent être dérivés.
Restez focalisé sur un périmètre restreint. Utilisez uniquement JSON, sans TypeScript ni Zod, ainsi que la détection des écarts, ce qui couvre les besoins essentiels. Tenter de gérer XML, protobuf et tous les cas limites des schémas transforme un outil précis en un outil imprécis. Pour une vue d’ensemble plus large des décisions relatives aux contrats qui nuisent souvent aux interfaces utilisateur, consultez les erreurs courantes des contrats API qui compromettent la fiabilité des interfaces utilisateur.
Premier test pratique
Le meilleur endroit pour essayer cela est une intégration qui a déjà été affectée par un changement silencieux de structure. Prenez une réponse ancienne et une réponse récente provenant du même point d’entrée, passez-les par l’inférence et une comparaison de snapshots, puis examinez ce qui est signalé. Voir un véritable changement historique apparaître dans la différence est plus convaincant que n’importe quel argument en faveur de cette approche.
Points clés
- Les interfaces manuscrites pour des API tierces ne sont que des captures du passé, et TypeScript ne peut pas déterminer quand elles deviennent obsolètes.
- Déduisez les types et les schémas Zod à partir de plusieurs réponses réelles, car plusieurs exemples révèlent des champs optionnels, nuls ou incohérents que seul un exemple masque.
- Vérifiez les réponses en temps de exécution, au niveau de l’API, afin que tout changement de structure provoque une erreur visible plutôt que de générer
undefined. - Stockez les réponses sous forme de captures et comparez les nouveaux échantillons avec elles ; la déduction automatique seule peut masquer un changement de nom en deux champs optionnels.
- Gardez les données utilisées en production locales, et préférez OpenAPI ou des tests de contrat chaque fois que vous contrôlez les deux côtés de l’API.
Lectures complémentaires
- Détecter le dérive du contract API en tempête de compilation grâce à un déploiement incrémental de tRPC — Comment tRPC transforme un champ backend renommé en erreur de compilation, comment l’adopter endpoint par endpoint à côté de REST, et dans quels cas c’est le mauvais outil.
- Six techniques TypeScript qui transforment les types en vraie prévention des bug — Découvrez comment satisfies, les unions étiquetées, never checks, unknown, les types dérivés et les IDs marqués permettent à TypeScript de détecter de vrais bugs en tempête de compilation plutôt qu’en production.