Scroll Top
Via Antonio Amato, 20/22 84131 Salerno (SA)

REST API: Best Practices per la Progettazione

Progettare una Rest API

(articolo redatto da Maurizio Migliore)

Per uno sviluppatore, creare un servizio REST è diventato quasi un gioco da ragazzi. I framework a disposizione, permettono in pochi passi di tirare su uno stack di codice a partire da una tabella presente su una base dati fino ad arrivare al Controller Rest.

Andrebbe però messo l’accento sulla progettazione iniziale della Rest API visto che, a tutti gli effetti, si tratta di un contratto che si stipula con i sistemi che vogliono integrarsi con il proprio software. Una volta definito il contratto sarà difficile tornare indietro e chiedere agli utilizzatori di adeguarsi a cuor leggero.

In questo articolo, si analizzano alcune problematiche comuni e per ognuna si fornisce un possibile approccio.

Versioning

Una pratica comune è includere il numero di versione nell’URL per consentire aggiornamenti, lasciando funzionanti i client esistenti. Per fare ciò, però, è importante individuare quale “livello” della API versionare.

Per spiegare meglio il concetto, in alcuni casi, capita di individuare un legame gerarchico tra una risorsa padre e una risorsa figlia. Va deciso se versionare il livello padre o il figlio.

Ad esempio, se avessimo la seguente gerarchia, andrebbe deciso se versionare al livello di product oppure singolarmente computer e printer:

  • api/product/computer → api/product/v1/computer oppure api/product/computer/v1
  • api/product/printer → api/product/v1/printer oppure api/product/printer/v1

Nel secondo caso, sarà possibile far evolvere soltanto la versione (e quindi il contratto e il funzionamento) di una risorsa figlia (es. computer) indipendentemente dalle altre.

Rappresentazione delle References

Quando si trattano le relazioni tra entità, è importante decidere come gestire le references. Una pratica comune è includere un collegamento all’entità relazionata anziché i dettagli completi. Ad esempio, se un oggetto fa riferimento a un altro, è possibile rappresentare la reference come un ID e fornire un endpoint separato per ottenere i dettagli completi dell’entità relazionata attraverso l’uso di collegamenti ipertestuali (HATEOAS – Hypermedia As The Engine Of Application State). Per raggiungere questo scopo conviene rappresentare il riferimento ad un’altra entità come un oggetto e non come un campo singolo.

Es. se l’entità book ha un riferimento ad una category:

Nomi dei fields: chiarezza e coerenza

I nomi dei campi dovrebbero essere autoesplicativi e seguire una formattazione coerente. È consigliato evitare le abbreviazioni e utilizzare un pattern di formattazione come ad esempio il camel case, dove la prima parola inizia con una lettera minuscola e le successive iniziano con lettere maiuscole: ad esempio è preferibile utilizzare “userName” al posto di “user” o “usrname”. Questo rende i nomi dei campi facilmente comprensibili e contribuisce alla coerenza nell’intera API.

Campi non esistenti o non modificabili: ignorarli o restituire errore?

Quando viene richiamato un method HTTP di scrittura (POST, PUT, PATCH) e nel body vengono specificati campi non esistenti o non modificabili, vi sono due alternative: bloccare l’operazione restituendo un errore oppure ignorarli.

Nel primo caso, il client sarà consapevole di aver inserito dati non gestiti o non modificabili. Di contro, ignorandoli, e adottando il comportamento di default di molte librerie (es. Jackson) sì ha il vantaggio di essere retrocompatibili. Ad esempio, se la versione 2 di una API rimuove un campo, il client che utilizza la versione 1 sarà ancora compatibile con la v2.

Deserializzazione dei tipi primitivi Loose o Strict?

La deserializzazione dei dati inviati dal client può essere gestita in modo “loose” o “strict”. Quando si utilizza una deserializzazione Loose per i tipi primitivi (ad esempio interi e booleani), la libreria potrebbe essere in grado di convertire una gamma più ampia di rappresentazioni testuali in valori validi del tipo primitivo, anche se non seguono rigorosamente le specifiche del linguaggio. Si consiglia la modalità “strict” per evitare ambiguità.

Ad esempio, un campo booleano in modalità loose può essere valorizzato con una stringa contenente il valore “True” oppure con un intero 1. Nella modalità strict sono accettati soltanto i valori true e false senza virgolette.

Jackson prevede il settaggio di alcuni parametri di configurazione via codice per abilitare la modalità strict: bisogna disabilitare MapperFeature.ALLOW_COERCION_OF_SCALARS per accettare valori strict su booleani e interi.

Prevedere anche una modifica parziale di un elemento

Il protocollo HTTP prevede due verbi per la modifica: PUT per la replace, PATCH per la modifica parziale.

Invece di utilizzare un DTO (Data Transfer Object) per specificare i campi da aggiornare, si può considerare l’uso di una Map<String, Object>. Questo approccio consente una maggiore flessibilità, consentendo agli utenti di inviare solo i campi che desiderano modificare, senza la necessità di creare DTO specifici per ogni possibile combinazione di campi. Per fare il merge dei valori, si consiglia di deserializzare la Map utilizzando la stessa libreria utilizzata dalle Rest API (es. Jackson), in modo da avere un comportamento uniforme.

Per fare un esempio, se la risorsa book ha i campi “title”, “author”, “price” e voglio modificare soltanto il prezzo di un libro identificato dal codice “BOOK01”:

dovrò invocare il l’url /api/book/BOOK01 con il metodo PATCH e con body:

CQRS: separare query da command

CQRS, Command Query Responsibility Segregation, è un pattern architetturale in cui le operazioni di lettura (query) e scrittura (command) sui dati sono separate. Per implementarlo in una REST API, si creano due distinti set di endpoint: uno per i comandi (POST, PATCH, PUT, DELETE) che modificano lo stato dei dati, e un altro per le query (GET) che li leggono. Questo permette ottimizzazioni specifiche, come scalabilità e prestazioni migliori per le query, e maggiore coerenza e sicurezza per i comandi.

Dove effettuare le validazioni?

In generale conviene non inserire logica di business sul front-end. I controller Rest che vanno visti come una sorta di “front-end”, vale a dire, una interfaccia verso l’esterno. Allo stesso modo, le validazioni relative al business di dominio vanno lasciate in un livello più basso, vicino alla base dati. Questo perché, se dovesse cambiare la modalità di accesso alla risorsa (es. da Rest API a Kafka), le operazioni di scrittura non ne risentirebbero.

Conclusioni

In conclusione, lo sviluppo di un’API REST richiede un approccio olistico e metodico, che va ben oltre la semplice implementazione tecnica. È fondamentale che gli sviluppatori e gli architetti considerino gli aspetti citati già nella fase di progettazione dell’API, per evitare costosi adeguamenti post-deployment e garantire una solida base per il futuro sviluppo del software.


Se anche tu vuoi occuparti di progetti di Sviluppo Applicazioni Enterprise
dai un’occhiata alle nostre opportunità di lavoro e conosciamoci subito!