Versionamento delle API per app mobile: far evolvere gli endpoint in modo sicuro
Versionamento delle API per app mobile spiegato con un piano di rollout semplice, modifiche retrocompatibili e passaggi di deprecazione così le versioni vecchie continuano a funzionare.

Perché le modifiche all'API rompono gli utenti mobili
Le app mobili non si aggiornano tutte nello stesso momento. Anche se pubblicate una correzione oggi, molte persone continueranno a usare una versione più vecchia per giorni o settimane. Alcuni disattivano gli aggiornamenti automatici. Alcuni hanno spazio di archiviazione limitato. Altri semplicemente non aprono l'app spesso. I tempi di revisione degli store e i rilasci graduali aggiungono ulteriori ritardi.
Questo divario conta perché il backend di solito evolve più velocemente dei client mobili. Se il server modifica un endpoint e l'app vecchia continua a chiamarlo, l'app può rompersi anche se sul telefono dell'utente non è cambiato nulla.
I malfunzionamenti raramente appaiono come un errore chiaro. Di solito si manifestano come problemi di prodotto quotidiani:
- Login o registrazione che falliscono dopo un rilascio backend
- Liste vuote perché un campo è stato rinominato o spostato
- L'app crasha quando legge un valore mancante
- I pagamenti falliscono perché la validazione è più rigida
- Funzionalità che scompaiono silenziosamente perché la forma della risposta è cambiata
Lo scopo del versionamento è semplice: continua a migliorare il server senza costringere tutti ad aggiornare immediatamente. Tratta la tua API come un contratto a lungo termine. Le nuove versioni dell'app dovrebbero funzionare con il comportamento del server aggiornato, e le versioni più vecchie dovrebbero restare operative abbastanza a lungo per coprire i cicli reali di aggiornamento.
Per la maggior parte delle app consumer, aspetta di supportare più versioni dell'app contemporaneamente. Le app interne a volte possono muoversi più velocemente, ma quasi mai istantaneamente. Pianificare la sovrapposizione mantiene i rollout graduali tranquilli invece di trasformare ogni rilascio backend in un picco di supporto.
Cosa significa “compatibile” per un contratto API
Un contratto API è la promessa tra la tua app mobile e il server: quale URL chiamare, quali input sono accettati, come appare la risposta e cosa significa ogni campo. Quando l'app si affida a quella promessa e il server la cambia, gli utenti lo percepiscono come crash, dati mancanti o funzionalità interrotte.
Una modifica è compatibile quando le versioni più vecchie dell'app possono continuare a usare l'API senza cambiare il codice. In pratica significa che il server continua a comprendere ciò che le app vecchie inviano e a restituire risposte che quelle app possono parsare.
Un modo veloce per separare i cambiamenti sicuri da quelli rischiosi:
- Cambiamenti breaking: rimuovere o rinominare un campo, cambiare un tipo (numero a stringa), rendere obbligatorio un campo opzionale, cambiare il formato degli errori, stringere la validazione in modo che le vecchie app non lo soddisfino.
- Cambiamenti generalmente sicuri: aggiungere un nuovo campo opzionale, aggiungere un nuovo endpoint, accettare sia il vecchio sia il nuovo formato di richiesta, aggiungere nuovi valori a un enum (solo se l'app tratta valori sconosciuti come “altro”).
La compatibilità richiede anche un piano di fine vita. Ritirare comportamenti vecchi va bene, ma dovrebbe essere programmato (ad esempio: “mantieni v1 per 90 giorni dopo il lancio di v2”) così puoi evolvere senza sorprendere gli utenti.
Approcci comuni al versionamento e compromessi
Il versionamento serve a dare ai build più vecchi un contratto stabile mentre tu procedi. Ci sono alcuni approcci comuni, e ognuno sposta la complessità in posti diversi.
Versioning nell'URL
Mettere la versione nel percorso (come /v1/ e /v2/) è il più facile da vedere e debuggare. Funziona bene anche con caching, logging e routing perché la versione fa parte dell'URL. Lo svantaggio è che i team possono ritrovarsi a mantenere handler paralleli più a lungo del previsto, anche quando la differenza è minima.
Versioning via header
Con il versioning via header, il client invia la versione in un header (ad esempio un header Accept o un header custom). Gli URL restano puliti e puoi evolvere l'API senza cambiare ogni percorso. Lo svantaggio è la visibilità: proxy, log e persone spesso non vedono la versione a meno di prestare attenzione, e i client mobili devono impostare l'header correttamente ad ogni richiesta.
Versioning via parametro di query
Il versioning tramite query (come ?v=2) sembra semplice, ma può diventare disordinato. I parametri vengono copiati in bookmark, strumenti di analytics e script, e si può finire con più “versioni” sparse senza una chiara ownership.
Se vuoi un confronto semplice:
- URL versioning: il più facile da ispezionare, ma può creare API parallele di lunga durata
- Header versioning: URL puliti, ma più difficile da risolvere i problemi
- Query versioning: rapido da avviare, facile da usare in modo errato
I feature flag sono uno strumento diverso. Permettono di cambiare comportamento dietro lo stesso contratto (ad esempio un nuovo algoritmo di ordinamento) senza creare una nuova versione API. Non sostituiscono il versionamento quando la forma di richiesta o risposta deve cambiare.
Scegli un approccio e mantienilo. La coerenza conta più della scelta “perfetta”.
Regole pratiche per cambiamenti retrocompatibili
La mentalità più sicura è: i client più vecchi devono continuare a funzionare anche se non conoscono la tua nuova feature. Questo di solito significa aggiungere cose, non modificare ciò che già esiste.
Preferisci cambiamenti additivi: nuovi campi, nuovi endpoint, nuovi parametri opzionali. Quando aggiungi qualcosa, rendila realmente opzionale dal punto di vista del server. Se un'app più vecchia non lo invia, il server dovrebbe comportarsi esattamente come prima.
Alcune abitudini che prevengono la maggior parte dei guasti:
- Aggiungi campi, ma non cambiare il tipo o il significato di quelli esistenti.
- Tratta gli input mancanti come normali e usa valori di default sensati.
- Ignora i campi di richiesta sconosciuti così vecchi e nuovi client possono coesistere.
- Mantieni stabile il formato degli errori. Se devi cambiarlo, versiona il payload di errore.
- Se il comportamento deve cambiare, introduci un nuovo endpoint o una nuova versione invece di una modifica “silenziosa”.
Evita di cambiare il significato di un campo esistente senza un bump di versione. Per esempio, se status=1 indicava “paid” e lo reinterpreti come “authorized”, le app vecchie prenderanno decisioni sbagliate e potresti non accorgertene finché gli utenti non si lamentano.
Rinomi e rimozioni richiedono un piano. Il pattern più sicuro è mantenere il campo vecchio e aggiungere quello nuovo fianco a fianco per un periodo. Popola entrambi nelle risposte, accetta entrambi nelle richieste e registra chi usa ancora il campo vecchio. Rimuovi il campo solo quando la finestra di deprecazione è scaduta.
Un'abitudine piccola ma potente: quando introduci una nuova regola di business obbligatoria, non rendere il client responsabile da subito. Applica la regola sul server con un default, poi più avanti richiedi che il client invii il nuovo valore una volta che la maggior parte degli utenti si è aggiornata.
Stabilisci una policy semplice di versioning e deprecazione
Il versionamento funziona meglio quando le regole sono noiose e messe per iscritto. Mantieni la policy abbastanza corta perché prodotto, mobile e backend la seguano davvero.
Inizia con le finestre di supporto. Decidi quanto a lungo manterrai le versioni API precedenti dopo il lancio di una nuova versione (ad esempio 6-12 mesi), più eccezioni (problemi di sicurezza, cambiamenti legali).
Poi definisci come avvisi i client prima di romperli. Scegli un segnale di deprecazione e usalo ovunque. Opzioni comuni includono un header di risposta come Deprecation: true con la data di ritiro, o un campo JSON come "deprecation": {"will_stop_working_on": "2026-04-01"} in risposte selezionate. Ciò che conta è la coerenza: i client possono rilevarlo, le dashboard possono segnalarlo e i team di supporto possono spiegarlo.
Stabilisci una versione minima supportata e sii esplicito su come la applicherai. Evita blocchi improvvisi. Un approccio pratico è:
- Restituire un avviso soft (ad esempio un campo che attiva un prompt in-app per aggiornare).
- Applicare l'enforcement solo dopo una scadenza comunicata.
Se blocchi richieste, restituisci un payload di errore chiaro con un messaggio umano e un codice leggibile dalla macchina.
Infine, decidi chi può approvare cambiamenti breaking e quale documentazione è richiesta. Mantienilo semplice:
- Un owner approva i breaking change.
- Una breve nota di cambio spiega cosa è cambiato, chi è interessato e il percorso di migrazione.
- Un piano di test include almeno una versione app più vecchia.
- Una data di ritiro è fissata quando inizia la deprecazione.
Piano di rollout step-by-step che mantiene operative le app vecchie
Gli utenti mobili non si aggiornano tutti il giorno stesso. L'approccio più sicuro è distribuire una nuova API lasciando intatta la vecchia, poi spostare il traffico gradualmente.
Prima, definisci cosa cambia in v2 e blocca il comportamento di v1. Tratta v1 come una promessa: stessi campi, stessi significati, stessi codici di errore. Se v2 ha una forma di risposta diversa, non modificare v1 per adattarla a v2.
Poi esegui v2 in parallelo. Questo può significare rotte separate (come /v1/... e /v2/...) o handler separati dietro lo stesso gateway. Mantieni la logica condivisa in un solo posto, ma conserva il contratto separato così una refactor di v2 non può cambiare accidentalmente v1.
Quindi aggiorna l'app mobile per preferire v2. Implementa un semplice fallback: se v2 restituisce “not supported” (o un altro errore conosciuto), ritenta con v1. Questo aiuta durante i rilasci graduali e quando le reti reali sono imprevedibili.
Dopo il rilascio dell'app, monitora adozione ed errori. Controlli utili includono:
- volume richieste v1 vs v2 per versione app
- tasso di errori e latenza per v2
- fallimenti di parsing delle risposte
- crash legati a schermate di rete
Quando v2 è stabile, aggiungi avvisi di deprecazione chiari per v1 e comunica una timeline. Ritira v1 solo quando l'utilizzo scende sotto una soglia accettabile (ad esempio sotto 1-2% per più settimane).
Esempio: cambi GET /orders per supportare filtri e nuovi status. v2 aggiunge status_details mentre v1 resta identica. La nuova app chiama v2, ma se incontra un caso limite ricade su v1 e continua a mostrare la lista ordini.
Suggerimenti di implementazione sul lato server
La maggior parte dei guasti durante i rollout avvengono perché la gestione delle versioni è sparsa tra controller, helper e codice di database. Mantieni la decisione “che versione è questa richiesta?” in un solo punto e rendi il resto della logica prevedibile.
Metti il routing di versione dietro una singola porta d'ingresso
Scegli un segnale (segmento URL, header o numero di build dell'app) e normalizzalo subito. Instrada al giusto handler in un modulo o middleware così ogni richiesta segue lo stesso percorso.
Un pattern pratico:
- Parsare la versione una sola volta (e loggarla).
- Mappare la versione a un handler (v1, v2, ...) in un'unica registry.
- Tenere le utility condivise version-agnostic (parsing date, controlli auth), non la logica della forma di risposta.
Fai attenzione quando condividi codice tra versioni. Correggere un bug in v2 nel codice “condiviso” può cambiare accidentalmente il comportamento di v1. Se una logica influenza i campi di output o le regole di validazione, mantienila versionata o coprila con test specifici per versione.
Mantieni i cambiamenti ai dati compatibili durante il rollout
Le migrazioni DB devono funzionare per entrambe le versioni contemporaneamente. Aggiungi colonne prima, backfilla se necessario, e solo dopo rimuovi o stringi i vincoli. Evita di rinominare o cambiare significati durante il rollout. Se devi cambiare formato, considera di scrivere entrambi i formati per un breve periodo finché la maggior parte dei client non si sposta.
Rendi gli errori prevedibili. Le app più vecchie spesso interpretano errori sconosciuti come “qualcosa è andato storto”. Usa codici di stato coerenti, identificatori di errore stabili e messaggi brevi che aiutino il client a decidere cosa fare (retry, re-auth, mostrare prompt di aggiornamento).
Infine, proteggi contro campi mancanti che le app vecchie non inviano. Usa default sicuri e valida con dettagli di errore chiari e stabili.
Considerazioni nell'app mobile che influenzano il versionamento
Poiché gli utenti possono restare su build vecchie per settimane, il versionamento deve presumere che più versioni client colpiranno il server contemporaneamente.
Un grande vantaggio è la tolleranza lato client. Se l'app crasha o fallisce il parsing quando il server aggiunge un campo, sentirai bug “casuali” durante il rollout.
- Ignora i campi JSON sconosciuti.
- Tratta i campi mancanti come normali e usa default.
- Gestisci i null in sicurezza (i campi possono diventare nullable durante le migrazioni).
- Non dipendere dall'ordine degli array a meno che il contratto non lo garantisca.
- Mantieni l'handling degli errori user-friendly (uno stato di retry è meglio di una schermata bianca).
Il comportamento di rete conta: durante il rollout puoi avere brevemente versioni server miste dietro bilanciatori o cache, e le reti mobili amplificano piccoli problemi.
Scegli timeout e retry chiari: timeout brevi per le letture, leggermente più lunghi per gli upload, e retry limitati con backoff. Rendi l'idempotenza uno standard per chiamate di creazione o pagamento così un retry non duplichi l'azione.
Le modifiche all'autenticazione sono il modo più rapido per bloccare app vecchie. Se cambi il formato del token, gli scope richiesti o le regole di sessione, mantieni una finestra di sovrapposizione dove sia accettato sia il vecchio sia il nuovo token. Se devi ruotare chiavi o claim, pianifica una migrazione graduale, non un cutover nello stesso giorno.
Invia metadata dell'app con ogni richiesta (ad esempio versione app e piattaforma). Questo rende più facile restituire avvisi mirati senza forkare tutta l'API.
Monitoraggio e rollout a fasi senza sorprese
Un rollout a fasi funziona solo se vedi cosa fanno le diverse versioni app. L'obiettivo è semplice: sapere chi è ancora su endpoint vecchi e intercettare problemi prima che raggiungano tutti.
Inizia tracciando l'uso per versione API ogni giorno. Non limitarti a contare le richieste totali. Traccia dispositivi attivi e segmenta endpoint chiave come login, profilo e pagamenti. Questo ti dice se una versione vecchia è ancora “viva” anche se il traffico complessivo sembra basso.
Poi osserva gli errori divisi per versione e tipo. Un aumento dei 4xx spesso indica mismatch di contratto (campo richiesto cambiato, valori enum spostati, regole auth più rigide). Un aumento dei 5xx spesso punta a regressioni server (deploy fallito, query lente, dipendenze in errore). Vederli per versione ti aiuta a scegliere la correzione giusta rapidamente.
Usa i rollout graduali negli store per limitare il raggio d'azione. Aumenta l'esposizione a passi e osserva le stesse dashboard dopo ogni step (ad esempio dopo 5%, 25%, 50%). Se la nuova versione mostra problemi, ferma il rollout prima che diventi un outage globale.
Hai dei trigger di rollback scritti prima dell'incidente, non decisi durante. Trigger comuni includono:
- tasso di errori sopra una soglia per 15-30 minuti
- diminuzione del tasso di login (o aumento dei fallimenti di refresh token)
- aumento dei fallimenti nei pagamenti
- picco di ticket di supporto legati a una versione specifica
- aumento della latenza su endpoint critici
Mantieni un playbook di incidenti breve per outage legati alle versioni: chi pageare, come disabilitare un flag rischioso, quale release server rollbackare e come estendere la finestra di deprecazione se i client vecchi sono ancora attivi.
Esempio: evolvere un endpoint durante un rilascio reale
Il checkout è un cambiamento classico. Parti con un flusso semplice, poi aggiungi un nuovo step di pagamento (ad esempio autenticazione più forte) e rinomini i campi per allinearli al linguaggio di business.
Supponiamo che la mobile app chiami POST /checkout.
Cosa rimane in v1 e cosa cambia in v2
In v1, mantieni la richiesta e il comportamento esistenti così le build più vecchie possono completare i pagamenti senza sorprese. In v2, introduci il nuovo flusso e nomi più chiari.
- v1 mantiene:
amount,currency,card_tokene una risposta unica comestatus=paid|failed. - v2 aggiunge:
payment_method_id(che sostituiscecard_token) e un camponext_actioncosì l'app può gestire un passaggio aggiuntivo (verify, retry, redirect). - v2 rinomina:
amountintotal_amountecurrencyinbilling_currency.
Le app più vecchie continuano a funzionare perché il server applica default sicuri. Se una richiesta v1 non conosce next_action, il server completa il pagamento quando possibile e restituisce lo stesso risultato in stile v1. Se il nuovo step è obbligatorio, v1 riceve un codice di errore chiaro e stabile come requires_update invece di un generico fallimento confuso.
Adozione, ritiro e rollback
Traccia l'adozione per versione: quale quota delle chiamate checkout raggiunge v2, i tassi di errore e quanti utenti usano ancora build che supportano solo v1. Quando l'uso di v2 è costantemente alto (ad esempio 95%+ per diverse settimane) e l'uso di v1 è basso, scegli una data di ritiro per v1 e comunica (note di rilascio, messaggi in-app).
Se qualcosa va storto dopo il lancio, il rollback dovrebbe essere banale:
- Reindirizza più traffico al comportamento v1.
- Disabilita il nuovo step di pagamento con un flag server-side.
- Continua ad accettare entrambi gli insiemi di campi e logga le conversioni automatiche.
Errori comuni che causano rotture silenziose
La maggior parte dei fallimenti delle API mobili non è rumorosa. La richiesta ha successo, l'app continua a funzionare, ma gli utenti vedono dati mancanti, totali errati o pulsanti che non fanno nulla. Questi problemi sono difficili da individuare perché spesso colpiscono versioni app più vecchie durante un rollout graduale.
Cause comuni:
- Cambiare o rimuovere campi (o il loro tipo) senza un piano di versioning chiaro.
- Rendere subito obbligatorio un nuovo campo di richiesta, così le app vecchie iniziano a essere rifiutate.
- Pubblicare una migrazione DB che presuppone l'esistenza solo della nuova app.
- Ritirare v1 basandosi sulle installazioni, non sull'uso attivo.
- Dimenticare job in background e webhook che ancora inviano payload vecchi.
Un esempio concreto: il tuo campo di risposta total era una stringa ("12.50") e lo cambi a numero (12.5). Le nuove app vanno bene. Le app più vecchie potrebbero interpretarlo come zero, nasconderlo o crashare solo su certe schermate. Se non guardi gli errori client per versione app, può scivolare inosservato.
Checklist rapida e prossimi passi
Il versionamento riguarda meno il nome elegante degli endpoint e più il ripetere gli stessi controlli di sicurezza a ogni release.
Controlli rapidi prima del rilascio
- Mantieni i cambiamenti additivi. Non rimuovere o rinominare campi che le app vecchie già leggono.
- Fornisci default sicuri così i campi nuovi mancanti si comportano come il flusso precedente.
- Mantieni stabili le risposte di errore (stato + forma + significato).
- Tratta gli enum con attenzione e non cambiare il significato di un valore esistente.
- Riprova alcune richieste reali da versioni app vecchie e conferma che le risposte si parsano.
Controlli rapidi durante il rollout e prima del ritiro
- Traccia l'adozione per versione app. Vuoi una curva chiara da v1 a v2, non una linea piatta.
- Osserva i tassi di errore per versione. Un picco spesso indica parsing o validazione che rompe i client vecchi.
- Risolvi prima gli endpoint con più errori, poi amplia il rollout.
- Ritira solo quando l'uso attivo è davvero basso e comunica la data.
- Rimuovi il codice di fallback per ultimo, dopo la finestra di ritiro.
Scrivi la tua policy di versioning e deprecazione in una pagina, poi trasforma la checklist in un gate di rilascio che il team segue ogni volta.
Se costruisci tool interni o app customer-facing con una piattaforma no-code, aiuta comunque trattare l'API come un contratto con una finestra di deprecazione chiara. Per team che usano AppMaster (appmaster.io), mantenere v1 e v2 affiancate è spesso più semplice perché puoi rigenerare backend e client man mano che i requisiti cambiano mantenendo i contratti vecchi operativi durante il rollout.
FAQ
Gli utenti mobili non aggiornano tutti nello stesso istante, quindi build più vecchie continuano a chiamare il backend dopo il deploy. Se cambi un endpoint, la validazione o la forma della risposta, quelle build più vecchie non possono adattarsi e falliscono in modi che sembrano schermate vuote, crash o pagamenti non completati.
“Compatibile” significa che un'app più vecchia può continuare a fare le stesse richieste e ricevere risposte che riesce a parsare e utilizzare correttamente, senza modifiche al codice. Il modello mentale più sicuro è trattare l'API come un contratto: puoi aggiungere funzionalità, ma non cambiare il significato dei campi o dei comportamenti esistenti per i client correnti.
Una modifica è breaking quando altera qualcosa di cui un'app esistente dipende: rimuovere o rinominare campi, cambiare il tipo di un campo, rendere più rigorosa la validazione in modo che le vecchie richieste falliscano, o cambiare il formato degli errori. Se un'app più vecchia non riesce a parsare la risposta o a soddisfare le regole della richiesta, è una rottura anche se il server sembra “funzionare”.
Il versioning via URL è spesso la scelta più semplice perché la versione è visibile in log, strumenti di debug e routing, ed è difficile “dimenticarsi” di inviarla. Il versioning via header funziona, ma è più facile trascurarlo durante il troubleshooting e richiede che ogni richiesta client imposti correttamente l'header.
Scegli una finestra di supporto che rifletta il comportamento reale degli aggiornamenti mobili e rispettala; molte squadre scelgono mesi, non giorni. L'importante è avere una data di ritiro pubblicata e misurare l'uso attivo, così non si spegne una versione basandosi solo sul numero di installazioni totali.
Usa un segnale di deprecazione coerente in tutta l'API in modo che client e dashboard possano rilevarlo in modo affidabile, ad esempio un header di risposta stabile o un piccolo campo JSON che includa la data di ritiro. Semplicità e prevedibilità aiutano supporto e prodotto a spiegare la situazione senza scavare nel codice.
Privilegia modifiche additive: aggiungi nuovi campi opzionali o nuovi endpoint e mantieni i campi vecchi funzionanti con lo stesso significato. Se devi rinominare, mantieni entrambi i campi in parallelo per un periodo, popolando entrambi così le app vecchie non perdono dati mentre quelle nuove passano al campo aggiornato.
Progetta le migrazioni in modo che entrambe le versioni API possano funzionare contemporaneamente: aggiungi colonne prima, riempi i dati se necessario e solo in seguito stringi i vincoli o rimuovi i campi vecchi. Evita di rinominare o cambiare significati durante il rollout, altrimenti una versione potrebbe scrivere dati che l'altra non riesce a leggere.
Rendi l'app tollerante: ignora campi JSON sconosciuti, tratta i campi mancanti come normali con valori di default sicuri e gestisci i null senza causare crash. Questo riduce i bug “a caso” durante il rollout quando il server aggiunge campi o le risposte variano temporaneamente.
Monitora uso ed errori per versione API e versione app, specialmente su login e pagamenti, e amplia il rollout solo quando i dati sono stabili. Un piano sicuro blocca il comportamento v1, esegue v2 in parallelo e sposta i client gradualmente con una strategia di fallback chiara finché l'adozione non è sufficientemente alta da ritirare v1 con fiducia.


