API REST: come identificare una risorsa? ..con gli UUID

API REST

Quando si progettano API REST il concetto di risorsa è fondamentale. Uno dei problemi principali che gli sviluppatori si pongono è infatti come fare ad identificare correttamente una risorsa. In questo articolo esploro le tecniche più comuni, con pro e contro di ognuna. Infine spiego il mio approccio preferito, che da sia flessibilità ma anche stabilità in contesti multi ambiente.

TLDR: se siete impazienti ecco i punti principali

  • Un'API REST può esporre le risorse utilizzando UUID come identificatori, in questo modo si evita all'API di essere facilmente enumerabile (pagando un pò di spazio in più per la memorizzazione).
  • Se la risorsa ha chiavi logiche ben definite, conviene utilizzare l'UUID-V3 generato proprio dalla chiave logica.
  • Se la risorsa non è identificata da chiavi logiche si può usare un UUID random, come l'UUID-V4
  • I servizi Backend interni dovrebbero usare e memorizzare anche la primary key immutabile (magari numerica ed auto-generata dal DB se il sistema è relazionale)

1. Chiavi Primarie auto-generate dal Database

Questo è uno degli identificatori più usati, specialmente se la risorsa viene trattata lato backend come una riga di un database relazione (o comune).

Sicuramente è il modo pià facile per identificare la risorsa REST, dato che il database ne garantisce l'unicità e immutabilità (si spera).

Troverete questi ID numerici sicuramente in servizi REST derivati da sistemi legacy o monolitici: per esempio una vecchia app che ha bisogno di una GUI più moderna o di esporre dati a terze parti. Di solito la risorsa assomiglierà ad un'URI del genere:

https://music.app/song/1

Di solito le risorse (canzoni in questo caso) sono in corrispondenza biunivoca con le righe di una tabella di un database relazionale. Nell'esempio riusciamo ad identificare la risorsa con l'id 1.

Vantaggi

  • Molto semplice, l'identificativo corrisponde di solito alla primary key e può essere auto-generato.
  • L'ID è obbligatorio, ed immutabile. Non è editabile, e questo è veramente un grande vantaggio.

Svantaggi

  • Potrebbe non essere appropriato (da solo) per sistemi distribuiti (o almeno se non corredato dal tipo di risorsa)
  • In contesti multi ambiente (es: dev, staging, produzione) la sincronizzazione è molto difficile. Avrete sempre ambienti con configurazioni dove magari la chiave logica è la stessa ma la chiave primaria no (perché auto-generata). In situazioni dove gli ambienti sono il punto di testing e UAT per i vostri clienti, sarà un inferno migrare dati e/o configurazioni (fidatevi, ci sono passato)
  • Dal punto di vista della sicurezza questa soluzione apre a scenari di enumerazione. Basta infatti cambiare l'identificatore numerico dell'API per scoprire ed enumerare le altre risorse.

2. Usare la Chiave Logica

Un'altra tecnica molto utilizzata è quella di identificare la risorsa REST con chiave logica. Di solito questo funziona molto bene per le risorse che possono essere trattate come un dominio finito.

Ad esempio i giorni della settimana, i nomi delle nazioni o le province italiane. Quindi delle buone risorse potrebbero assomigliare a:

https://my.app/weekday/monday
https://my.app/country/italy

Vantaggi

  • Interpretabile facilmente per utenti e sviluppatori delle API. L'identificatore suggerisce anche il contenuto della risorsa.
  • Più facile da sincronizzare tra i vari ambienti. Non ci sono sovrapposizioni di ID, o riferimenti errati.
  • La chiave logica può anche essere usata come rotta (route) nelle web app che fanno uso della risorsa (ad esempio una pagina di dettaglio sui dati esposti dall'API).

Svantaggi

  • Dobbiamo garantire l'univocità. Il backend quindi diventa più sofisticato, se sono permesse le modifiche.
  • Non è adatta (da sola) a sistemi relazionali, se hai intenzione di usarla come chiave primaria. A meno che il dominio non sia chiuso e garantita l'immutabilità.
  • Di solito le chiavi logiche sono mutabili. Quando modifichi una risorsa, e cambi la chiave logica, devi pensare a modificare anche tutti i riferimenti (FK).
  • Quando la chiave logica è composta, le cose si complicano. L'identificativo della risorsa deve essere generato a partire da più campi.

Quindi nella vita reale, è possibile trovare API REST che espongono le loro risorse mediante chiavi logiche, ma di solito lato backend vengono usate in ogni caso chiavi primarie univoche e immutabili per non avere problemi nei riferimenti e/o degradare le performance.

3. Chiavi logiche e UUID: memorizza la chiave univoca (PK), esponi un UUID-V3

Questa è la migliore opzione che mi sento di consigliare, e che uso sempre anche nelle mie API e nei servizi di cui sono responsabile.

  1. In generale, uso sempre chiavi primarie auto-generate magari dal database lato backend. Quindi i riferimenti (FK) sono sempre numerici e immutabili.
  2. Poi "decoro" uan risorsa con una chiave logica. Per esempio un codice o un nome, di solito stringa senza spazi o caratteri particolari. Per risorse complesse ricorro ad una chiave logica composta, concatenando i campi utili.
  3. Tutte le API REST che espongo, hanno come identificato un UUID-V3, generato a partire dal tipo di risorsa e dalla chiave logica.

UUID-V3 sono identificatori unici ed universali generati a partire dall'hash MD5 di un namespace e di un name (due stringhe). Di solito il namespace è a sua volta un altro UUID che identifica in modo univoco il servizio o l'app mentre il name è formato dalla tipologia di risorsa + la chiave logica.

Per esempio immagina un'applicazione che identifica gli utenti mediante username. Potresti esporre sia la chiave logica (username) e l'UUID-V3:

https://my.app/users/andrea
https://my.app/users/92068a6a-822a-3865-9970-bd4bc3e96f2c

// NB. the UUID è sempre lo stesso per l'utente andrea. Ho usato
// la stringa user_andrea come chiave logica
// https://uuidonline.com/?version=3&namespace=mysystem_andrea

Vantaggi

  • Il Backend usa una chiave primaria PK numerica, generata magari da un DB relazionale ed in modo seriale.
  • UUID-V3 sono ideali per la sincronizzazione verso l'esterno o altri ambienti. A partire dalla chiave logica l'UUID-V3 è sempre lo stesso. Si riesce subito ad identificare la risorsa, anche se tra ambienti diversi la chiave primaria cambia.
  • Puoi esporre sia un UUID-V3 (per risorse complesse) che la chiave logica (per risorse su domini chiusi). Puoi switchare tra i due a seconda del bisogno. Per esempio la GUI potrebbe usare una chiave logica.

Svantaggi

  • Il Backend è più sofisticato. Potresti dover esporre sia le chiavi logiche che gli UUID.
  • Quando aggiorni la chiave logica (se permesso dal tuo sistema) hai bisogno di modificare anche l'UUID. Ma dato che la chiave primaria (numerica auto-generata) non cambia, questa operazioni non è onerosa.

4. UUID Random: Se non ho una chiave logica allora usiamo UUID-v4

Capita molto spesso che le risorse non abbiamo una vera chiave logica associata. Ad esempio l'identificativo di una transazione, o di una sessione utente su una web app. In questo caso se lato backend si utilizza un sistema relazionale, la via più semplice è generare un id univoco auto-incrementato, come abbiamo visto al punto 2.

Però se non vogliamo legarci all'id di un database, o magari non stiamo usando un sistema tradizionale può avere senso generare un UUID randomico e in questo caso ci viene in aiuto la versione UUID-V4. Ad esempio:

https://my.app/orders/92068a6a-822a-1865-9870-ad4bc3e96f2a

Vantaggi

  • Molto semplice da implementare. L'UUID è random ed associato ad una singola risorsa.
  • L'approccio è ottimo se si vuole evitare l'enumerazione delle risorse da parte di chi utilizza l'API.

Svantaggi

  • La sincronizzazione tra ambienti diversi diventa difficile. Non abbiamo un modo per identificare se una risorsa è diversa da un'altra se gli ambienti generano UUID random e non ci sono chiavi logiche.
  • Se l'UUID è l'unica chiave primaria dovete fare attenzione a costruire indici complessi o join nei database tradizionali, le performance sicuramente sono inferiori rispetto a chiavi primarie numeriche e facilmente indicizzabili.

Anche qui se dovete usare questo approccio vi consigli lato backend di usare in ogni caso chiavi primarie univoche e immutabili per non avere problemi nei riferimenti e/o degradare le performance.

Per concludere

  • Gli UUID sono un ottimo approccio per identificare risorse REST, specialmente se non vi ponete problemi relativi allo spazio necessario alla loro memorizzazione e se li utilizzate solo come interfaccia verso l'esterno.
  • Avete a disposizione diverse versioni di UUID, fate attenzione a scegliere quella che più si adatta al vostro caso d'uso.
  • Se potete, ed ha senso, lato backend utilizzate comunque una chiave primaria numerica, ed utilizzate gli UUID solo come identificatore verso l'esterno.