Test dei contratti per API: prevenire modifiche incompatibili nei team veloci
I test dei contratti per API ti aiutano a intercettare modifiche incompatibili prima dei rilasci web e mobile. Passi pratici, errori da evitare e una checklist rapida per il deploy.

Perché le modifiche incompatibili alle API continuano a finire nelle release
La maggior parte dei team si ritrova con un'unica API che serve molti client: un'app web, un'app iOS, un'app Android e a volte anche strumenti interni. Anche se tutti concordano sugli “stessi” endpoint, ogni client si appoggia all'API in modi leggermente diversi. Una schermata potrebbe aspettarsi che un campo esista sempre, mentre un'altra lo usa solo quando è applicato un filtro.
Il vero problema emerge quando queste parti vengono rilasciate con tempi diversi. Il backend può andare in produzione più volte al giorno, il web si distribuisce velocemente e i rilasci mobile vanno più lenti a causa delle revisioni e dei rollout graduali. Quel divario crea rotture a sorpresa: l'API viene aggiornata per il client più recente, ma la build mobile di ieri è ancora in circolazione e ora riceve risposte che non riesce a gestire.
Quando succede, i sintomi raramente sono sottili:
- Una schermata che improvvisamente appare vuota perché un campo è stato rinominato o spostato
- Crash causati da null inaspettati o oggetti mancanti
- Ticket di supporto “Qualcosa non funziona” con passi difficili da riprodurre
- Un picco nei log di errore subito dopo un deploy del backend
- Hotfix che aggiungono codice difensivo invece di risolvere la causa principale
I test manuali e la QA spesso non catturano questi problemi perché i casi a rischio non sono il percorso felice. Un tester può verificare che “Crea ordine” funzioni, ma non provare una versione app più vecchia, un profilo parzialmente compilato, un ruolo utente raro o una risposta con una lista vuota. Aggiungi caching, feature flag e rollout graduali, e ottieni molte più combinazioni di quante un piano di test possa coprire.
Un esempio tipico: il backend sostituisce status: "approved" con status: { code: "approved" } per supportare la localizzazione. L'app web viene aggiornata lo stesso giorno e sembra a posto. Ma la release iOS corrente si aspetta ancora una stringa, non riesce a parsare la risposta e gli utenti vedono una pagina vuota dopo il login.
Ecco perché esistono i test dei contratti per le API: non per sostituire la QA, ma per catturare questi cambiamenti “funzionano per il mio client più recente” prima che raggiungano la produzione.
Cosa sono i test dei contratti (e cosa non sono)
I test dei contratti sono un modo per un consumer dell'API (un'app web, mobile o un altro servizio) e un provider dell'API (il tuo backend) di mettersi d'accordo su come comunicare. L'accordo è il contratto. Un test di contratto verifica una cosa semplice: il provider si comporta ancora come il consumer si aspetta, anche dopo le modifiche?
Nella pratica, il testing dei contratti si colloca tra i test unitari e i test end-to-end. I test unitari sono veloci e locali, ma possono perdere disallineamenti tra team perché testano codice interno, non il confine condiviso. I test end-to-end percorrono flussi reali attraverso molti sistemi, ma sono più lenti, difficili da mantenere e spesso falliscono per ragioni non correlate a un cambiamento API (dati di test, sincronizzazione UI, ambienti instabili).
Un contratto non è un documento enorme. È una descrizione mirata delle richieste che un consumer invierà e delle risposte che deve ricevere. Un buon contratto di solito copre:
- Endpoint e metodi (per esempio, POST /orders)
- Campi richiesti e opzionali, inclusi tipi e regole di base
- Codici di stato e forma delle risposte di errore (come sono 400 vs 404)
- Header e aspettative di auth (token presente, content type)
- Default importanti e regole di compatibilità (cosa succede se manca un campo)
Ecco un esempio semplice del tipo di rottura che un test di contratto intercetta presto: il backend rinomina total_price in totalPrice. I test unitari possono ancora passare. I test end-to-end potrebbero non coprire quella schermata o potrebbero fallire in modo confuso più avanti. Un test di contratto fallisce immediatamente e segnala la discrepanza esatta.
È utile chiarire cosa non fa il testing dei contratti. Non sostituisce i test di performance, di sicurezza o i test completi dei percorsi utente. Non catturerà ogni bug logico. Quello che fa è ridurre il rischio di rilascio più comune nei team veloci: una modifica “piccola” all'API che rompe silenziosamente un client.
Se il tuo backend è generato o cambia frequentemente (per esempio quando rigeneri API in una piattaforma come AppMaster), i test dei contratti sono una rete di sicurezza pratica perché verificano che le aspettative dei client tengano dopo ogni modifica.
Scegliere un approccio di contratto per team web e mobile
Quando web e mobile rilasciano spesso, la parte difficile non è “testare l'API”. È mettersi d'accordo su cosa non deve cambiare per ogni client. Qui entrano in gioco i test dei contratti, ma devi anche scegliere come viene gestito il contratto.
Opzione 1: contratti guidati dal consumer (CDC)
Con i contratti guidati dal consumer, ogni client (web, iOS, Android, integrazione partner) definisce ciò di cui ha bisogno dall'API. Il provider poi dimostra di poter soddisfare quelle aspettative.
Questo funziona bene quando i client si muovono indipendentemente, perché il contratto riflette l'uso reale, non ciò che il team backend pensa sia usato. Si adatta anche alla realtà multi-client: iOS può dipendere da un campo che il web non usa, e il web può curarsi di regole di ordinamento o paginazione che il mobile ignora.
Un esempio semplice: l'app mobile si basa su price_cents come intero. Il web mostra solo il prezzo formattato, quindi non noterebbe se il backend lo cambiasse in stringa. Un CDC dal mobile catturerebbe quel cambiamento prima del rilascio.
Opzione 2: schemi gestiti dal provider
Con uno schema gestito dal provider, il team backend pubblica un contratto unico (spesso uno schema o una spec) e lo applica. I consumer testano contro questa singola fonte di verità.
Questa scelta è adatta quando l'API è pubblica o condivisa tra molti consumer che non controlli, o quando hai bisogno di coerenza rigorosa tra i team. È anche più semplice da avviare: un contratto, un posto per le revisioni, un percorso di approvazione.
Ecco un modo rapido per scegliere:
- Scegli i CDC quando i client rilasciano frequentemente e usano porzioni diverse dell'API.
- Scegli schemi gestiti dal provider quando ti serve un unico contratto “ufficiale” stabile per tutti.
- Usa un ibrido quando possibile: uno schema provider per la baseline, più CDC per gli endpoint ad alto rischio.
Se costruisci con una piattaforma come AppMaster, lo stesso principio vale: considera web e app native come consumer separati. Anche quando condividono un backend, raramente dipendono esattamente dagli stessi campi e regole.
Cosa mettere in un contratto API (per catturare rotture reali)
Un contratto API aiuta solo se riflette ciò da cui web e mobile dipendono veramente. Una specifica elegante che nessuno usa non catturerà la modifica che rompe la produzione.
Parti dall'uso reale, non da ipotesi. Prendi le chiamate client più comuni (dal codice dell'app, dai log del gateway API o da una breve lista dei team) e trasformale in casi di contratto: il percorso esatto, il metodo, gli header, i query param e la tipica forma del body della richiesta. Questo mantiene il contratto piccolo, rilevante e difficile da contestare.
Includi risposte di successo e di errore. I team spesso testano i contratti per il percorso felice e dimenticano che anche i client si basano sugli errori: il codice di stato, la forma dell'errore e persino codici/messaggi di errore stabili. Se un'app mobile mostra un messaggio specifico “email già usata”, un contratto dovrebbe fissare quella risposta 409 in modo che non diventi improvvisamente un 400 con un body diverso.
Presta particolare attenzione alle aree che rompono più spesso:
- Campi opzionali vs richiesti: rimuovere un campo è di solito più sicuro che renderlo obbligatorio.
- Null: alcuni client trattano
nullin modo diverso da “mancante”. Decidi cosa permettere e mantienilo consistente. - Enum: aggiungere un nuovo valore può rompere client più vecchi che assumono una lista chiusa.
- Paginazione: concorda parametri e campi di risposta (come
cursoronextPageToken) e mantienili stabili. - Formati di data e numero: rendili espliciti (stringhe ISO, centesimi come interi, ecc.).
Come rappresentare il contratto
Scegli un formato che i team possano leggere e che gli strumenti possano validare. Opzioni comuni sono JSON Schema, contratti basati su esempi o modelli tipizzati generati da una spec OpenAPI. In pratica, esempi più un controllo di schema funzionano bene: gli esempi mostrano payload reali, mentre le regole di schema catturano errori come “campo rinominato” o “tipo cambiato”.
Una regola semplice: se una modifica costringerebbe un aggiornamento del client, dovrebbe far fallire un test di contratto. Questa mentalità mantiene i contratti focalizzati sulle rotture reali, non sulla perfezione teorica.
Passo dopo passo: aggiungere test di contratto alla pipeline CI
L'obiettivo dei test dei contratti è semplice: quando qualcuno cambia l'API, la tua CI ti dica se qualche client web o mobile si romperà prima che la modifica venga rilasciata.
1) Inizia catturando ciò su cui i client fanno realmente affidamento
Scegli un singolo endpoint e annota le aspettative che contano nell'uso reale: campi richiesti, tipi, valori consentiti, codici di stato e risposte di errore comuni. Non cercare di descrivere tutta l'API in una volta. Per le app mobile, includi anche le aspettative delle versioni più vecchie, dato che gli utenti non aggiornano istantaneamente.
Un modo pratico è prendere alcune richieste reali che i tuoi client fanno oggi (da log o fixture di test) e trasformarle in esempi ripetibili.
2) Metti i contratti dove i team li manterranno
I contratti falliscono quando vivono in una cartella dimenticata. Tienili vicino al codice che cambia:
- Se un team possiede entrambi i lati, archivia i contratti nel repo dell'API.
- Se team diversi possiedono web, mobile e API, usa un repo condiviso gestito dai team, non da una singola persona.
- Tratta gli aggiornamenti del contratto come codice: revisionati, versionati e discussi.
3) Aggiungi controlli su entrambi i lati nella CI
Vuoi due segnali:
- Verifica provider ad ogni build API: “L'API soddisfa ancora tutti i contratti noti?”
- Controlli consumer ad ogni build client: “Questo client è ancora compatibile con l'ultimo contratto pubblicato?”
Questo cattura problemi da entrambe le direzioni. Se l'API cambia un campo di risposta, la pipeline API fallisce. Se un client inizia ad aspettarsi un nuovo campo, la pipeline client fallisce finché l'API non lo supporta.
4) Decidi la regola di fallimento e applicala
Sii esplicito su cosa blocca una merge o un rilascio. Una regola comune è: qualsiasi cambiamento che rompe il contratto fa fallire la CI e blocca la merge nel branch principale. Se servono eccezioni, richiedi una decisione scritta (per esempio, una data di rilascio coordinata).
Esempio concreto: un cambio backend rinomina totalPrice in total_amount. La verifica provider fallisce immediatamente, così il team backend aggiunge il nuovo campo mantenendo il vecchio per un periodo di transizione, e sia web che mobile continuano a funzionare in sicurezza.
Versioning e retrocompatibilità senza rallentare i team
I team veloci rompono le API soprattutto cambiando ciò su cui i client esistenti già fanno affidamento. Una “breaking change” è qualsiasi cosa che fa fallire una richiesta che prima funzionava, o che rende la risposta significativamente diversa in un modo che il client non sa gestire.
Ecco le breaking change comuni (anche quando l'endpoint esiste ancora):
- Rimuovere un campo di risposta che i client leggono
- Cambiare il tipo di un campo (per esempio,
"total": "12"a"total": 12) - Rendere obbligatorio un campo opzionale (o aggiungere un nuovo campo richiesto nella richiesta)
- Cambiare le regole di auth (un endpoint pubblico richiede ora un token)
- Cambiare codici di stato o forma degli errori che i client parseano (200 a 204, o un nuovo formato di errore)
La maggior parte dei team può evitare bump di versione scegliendo alternative più sicure. Se ti servono più dati, aggiungi un nuovo campo invece di rinominarne uno. Se vuoi un endpoint migliore, aggiungi una nuova route e tieni quella vecchia funzionante. Se devi stringere le validazioni, accetta sia l'input vecchio che quello nuovo per un periodo, poi applica gradualmente le nuove regole. I test dei contratti aiutano qui perché ti costringono a dimostrare che i consumer esistenti ottengono ancora ciò che si aspettano.
La deprecazione è la parte che mantiene l'agilità alta senza danneggiare gli utenti. I client web possono aggiornare quotidianamente, ma le app mobile possono ritardare settimane per revisione in store e adozione lenta. Pianifica la deprecazione attorno al comportamento reale dei client, non alla speranza.
Una policy di deprecazione pratica:
- Annuncia il cambiamento presto (note di rilascio, canale interno, ticket)
- Mantieni il comportamento vecchio finché l'uso non scende sotto una soglia concordata
- Restituisci avvisi in header/log quando viene usato il percorso deprecato
- Imposta una data di rimozione solo dopo aver confermato che la maggior parte dei client ha aggiornato
- Elimina il comportamento vecchio solo dopo che i test di contratto mostrano che nessun consumer attivo ne ha più bisogno
Usa il versioning esplicito solo quando non puoi rendere il cambiamento retrocompatibile (per esempio, un cambiamento fondamentale nella forma della risorsa o nel modello di sicurezza). Il versioning aggiunge costi a lungo termine: ora mantieni due comportamenti, due set di documentazione e più edge case. Tienilo raro e deliberato, e usa i contratti per assicurarti che entrambe le versioni restino corrette finché la vecchia non è davvero sicura da rimuovere.
Errori comuni nei test dei contratti (e come evitarli)
I test dei contratti funzionano meglio quando controllano aspettative reali, non una versione giocattolo del tuo sistema. La maggior parte dei fallimenti deriva da alcuni schemi prevedibili che fanno sentire i team al sicuro mentre i bug continuano a scivolare in produzione.
Errore 1: trattare i contratti come “mock sofisticati”
L'over-mocking è la trappola classica: il test di contratto passa perché il comportamento del provider è stato mockato per corrispondere al contratto, non perché il servizio reale può realmente farlo. Al deploy, la prima chiamata reale fallisce.
Una regola più sicura è semplice: i contratti dovrebbero essere verificati contro il provider in esecuzione (o un artefatto di build che si comporta allo stesso modo), con serializzazione reale, validazione reale e regole di auth reali.
Ecco gli errori che appaiono più spesso e la correzione che funziona di solito:
- Over-mocking del comportamento del provider: verifica i contratti contro una build provider reale, non contro un servizio stub.
- Contratti troppo rigidi: usa matching flessibile per ID, timestamp e array; evita di asserire ogni campo se i client non se ne servono.
- Ignorare le risposte di errore: testa almeno i principali casi di errore (401, 403, 404, 409, 422, 500) e la forma del body di errore che il client parsea.
- Nessuna ownership chiara: assegna chi aggiorna il contratto quando cambiano i requisiti; rendilo parte della “definition of done” per le modifiche API.
- Dimenticare le realtà mobile: testa con reti più lente e versioni app più vecchie in mente, non solo con l'ultima build su Wi‑Fi veloce.
Errore 2: contratti fragili che bloccano cambiamenti innocui
Se un contratto fallisce ogni volta che aggiungi un campo opzionale o riorganizzi chiavi JSON, gli sviluppatori imparano a ignorare il build rosso. Questo vanifica lo scopo.
Punta a “rigore dove conta”. Sii severo su campi richiesti, tipi, valori enum e regole di validazione. Sii flessibile su campi extra, ordine e valori che variano naturalmente.
Un piccolo esempio: il tuo backend cambia status da "active" | "paused" a "active" | "paused" | "trial". Se un'app mobile tratta valori sconosciuti come un crash, questa è una breaking change. Il contratto dovrebbe intercettarla verificando come il client gestisce valori enum sconosciuti, oppure richiedendo al provider di restituire solo valori noti finché tutti i client non possono gestire il nuovo valore.
I client mobile meritano un'attenzione in più perché restano più a lungo in produzione. Prima di dichiarare un cambiamento “sicuro”, chiediti:
- Le versioni più vecchie dell'app riescono ancora a parsare la risposta?
- Cosa succede se la richiesta viene ritentata dopo un timeout?
- I dati in cache entreranno in conflitto col nuovo formato?
- Abbiamo un fallback quando manca un campo?
Se le tue API sono generate o aggiornate rapidamente (incluso con piattaforme come AppMaster), i contratti sono un guardrail pratico: ti permettono di muoverti velocemente dimostrando comunque che web e mobile continueranno a funzionare dopo ogni cambiamento.
Checklist rapida pre-ship per le modifiche API
Usa questa lista subito prima di fare merge o rilasciare una modifica API. È pensata per catturare le piccole modifiche che causano i peggiori incendi quando web e mobile rilasciano spesso. Se già fai testing dei contratti, questa lista ti aiuta a concentrarti sulle rotture che i contratti dovrebbero bloccare.
Le 5 domande da farsi ogni volta
- Abbiamo aggiunto, rimosso o rinominato campi di risposta che i client leggono (inclusi campi annidati)?
- Sono cambiati i codici di stato (200 vs 201, 400 vs 422, 404 vs 410), o è cambiato il formato del body di errore?
- Qualche campo è passato da opzionale a richiesto (incluso “può essere null” vs “deve essere presente”)?
- Sono cambiate ordinamento, paginazione o filtri predefiniti (dimensione pagina, ordine, token cursor)?
- I test di contratto sono stati eseguiti per il provider e per tutti i consumer attivi (web, iOS, Android e eventuali strumenti interni)?
Un esempio semplice: la tua API restituiva totalCount, e un client lo usa per mostrare “24 risultati”. Lo rimuovi perché “la lista ha già gli elementi”. Il backend non va in crash, ma l'UI mostra vuoto o “0 risultati” per alcuni utenti. Questa è una rottura reale, anche se l'endpoint restituisce ancora 200.
Se hai risposto “sì” a una qualsiasi domanda
Fai questi controlli rapidi prima di rilasciare:
- Conferma se i client vecchi funzionano ancora senza aggiornamento. In caso contrario, aggiungi un percorso retrocompatibile (mantieni il vecchio campo o supporta entrambi i formati per un periodo).
- Controlla la gestione degli errori nei client. Molte app trattano forme di errore sconosciute come “Qualcosa è andato storto” e nascondono messaggi utili.
- Esegui i test di contratto consumer per ogni versione client rilasciata che ancora supporti, non solo per il branch più recente.
Se costruisci strumenti interni velocemente (per esempio, un pannello admin), assicurati di includere anche quei consumer. In AppMaster, i team spesso generano app web e mobile dagli stessi modelli backend, il che facilita dimenticare che una piccola modifica dello schema può comunque rompere un client rilasciato se il contratto non è controllato in CI.
Esempio: intercettare una breaking change prima che web e mobile rilascino
Immagina una configurazione comune: il team API deploya più volte al giorno, il web rilascia quotidianamente e le app mobile settimanalmente (per revisione store e rollout). Tutti si muovono rapidamente, quindi il vero rischio non è l'intenzionalità, ma piccole modifiche che sembrano innocue.
Un ticket di supporto chiede nomi più chiari nella risposta del profilo utente. Il team API rinomina un campo in GET /users/{id} da phone a mobileNumber.
Quella rinomina sembra pulita, ma è una breaking change. Il client web potrebbe mostrare il numero di telefono vuoto nella pagina profilo. Peggio, il client mobile potrebbe andare in crash se considera phone obbligatorio, o fallire la validazione al salvataggio.
Con i test dei contratti, questo viene intercettato prima che raggiunga gli utenti. Ecco come tipicamente fallisce, a seconda di come esegui i controlli:
- Build provider fallisce (lato API): il job CI dell'API verifica il provider contro i contratti consumer salvati da web e mobile. Vede che i consumer si aspettano ancora
phone, ma il provider restituiscemobileNumber, quindi la verifica fallisce e il deploy viene bloccato. - Build consumer fallisce (lato client): il team web aggiorna il proprio contratto per richiedere
mobileNumberprima che l'API lo rilasci. Il loro test di contratto fallisce perché il provider non fornisce ancora quel campo.
In entrambi i casi, il fallimento è precoce, evidente e specifico: indica l'endpoint esatto e la discrepanza sul campo, invece di manifestarsi come “pagina profilo rotta” dopo il rilascio.
La soluzione è di solito semplice: rendere il cambiamento additivo, non distruttivo. L'API restituisce entrambi i campi per un periodo:
- Aggiungi
mobileNumber. - Mantieni
phonecome alias (stesso valore). - Segna
phonecome deprecato nelle note del contratto. - Aggiorna web e mobile per leggere
mobileNumber. - Rimuovi
phonesolo dopo aver verificato che tutte le versioni client supportate sono migrate.
Una timeline realistica sotto pressione di rilascio:
- Lun 10:00: il team API aggiunge
mobileNumbere mantienephone. I test provider passano. - Lun 16:00: il web passa a
mobileNumbere rilascia. - Gio: il mobile passa a
mobileNumbere invia la release. - Martedì successivo: la release mobile raggiunge la maggior parte degli utenti.
- Sprint seguente: l'API rimuove
phone, e i test di contratto confermano che nessun consumer supportato ne ha più bisogno.
Questo è il valore principale: i test di contratto trasformano la “roulette delle breaking change” in una transizione controllata e temporizzata.
Prossimi passi per team che si muovono veloci (inclusa un'opzione no-code)
Se vuoi che i test dei contratti prevengano davvero le rotture (e non siano solo un controllo in più), mantieni il rollout piccolo e chiarisci la ownership. L'obiettivo è semplice: intercettare le breaking change prima che colpiscano i rilasci web e mobile.
Inizia con un piano leggero. Scegli i 3 endpoint principali che causano più problemi quando cambiano, di solito auth, profilo utente e un endpoint core di lista/ricerca. Metti prima sotto contratto quelli, poi espandi quando il team si fida del flusso.
Un rollout pratico e gestibile:
- Settimana 1: test di contratto per i 3 endpoint principali, eseguiti su ogni pull request
- Settimana 2: aggiungi i successivi 5 endpoint con più utilizzo mobile
- Settimana 3: copri risposte di errore e casi limite (stati vuoti, errori di validazione)
- Settimana 4: rendi il “contract green” una guardia di rilascio per i cambi backend
Poi, decidi chi fa cosa. I team vanno più veloci quando è ovvio chi possiede un fallimento e chi approva una modifica.
Tieni i ruoli semplici:
- Proprietario del contratto: di solito il team backend, responsabile di aggiornare i contratti quando il comportamento cambia
- Revisori consumer: lead web e mobile che confermano che i cambiamenti sono sicuri per i loro client
- Build sheriff: rotazione giornaliera o settimanale, triage dei fallimenti dei test di contratto in CI
- Release owner: decide di bloccare un rilascio se un contratto è rotto
Traccia una sola metrica di successo che importi a tutti. Per molti team, il segnale migliore è meno hotfix dopo i rilasci e meno “regressioni client” come crash app, schermate vuote o checkout rotti legati a cambi API.
Se cerchi un feedback loop ancora più veloce, le piattaforme no-code possono ridurre la deriva rigenerando codice pulito dopo le modifiche. Quando logica o modelli dati cambiano, la rigenerazione aiuta a evitare l'accumulo lento di patch che cambiano accidentalmente il comportamento.
Se costruisci API e client con AppMaster, un passo pratico successivo è provare ora creando un'applicazione, modellando i dati nel Data Designer (PostgreSQL), aggiornando i workflow nel Business Process Editor, poi rigenerando e distribuendo sul tuo cloud (o esportando il codice sorgente). Abbinalo a controlli di contratto nella CI così ogni build rigenerata dimostra ancora di corrispondere a ciò che web e mobile si aspettano.


