Pochi principi per scrivere ottime API REST

Come sviluppatore web mi sono ritrovato spesso a scrivere o consumare API Rest e come tutte le cose che circondano il nostro mondo, non sempre sono scritte e strutturate nella maniera corretta (e anche io non le ho sempre scritte correttamente!). Un progetto ben realizzato e mantenibile nel corso del tempo passa anche da questi aspetti e padroneggiare questi argomenti fa la differenza tra un progetto buono e uno mal realizzato.

Come dico spesso, noi sviluppatori siamo artigiani dell’intangibile e la qualità del nostro lavoro passa anche dalla “maestria” con la quale scolpiamo le nostre applicazioni.

Questo articolo non è un punto di arrivo, ma bensì un punto di partenza, di passaggio, un insieme di raccolte di best practice e di linee guida da conoscere per scrivere delle buone API.

Perché scriviamo API?

La risposta è semplice: qualcuno (o qualcosa) deve interagire con il nostro sistema, qualunque esso sia. Il nostro compito è di costruire una strada di accesso verso il nostro sistema: sareste contenti di percorrere una strada dissestata e piena di buche e che cambia spesso? Io personalmente no, scrivere delle buone API REST significa costruire una buona strada.

La miglior api è quella che gli sviluppatori possono usare senza leggere la documentazione

Pensa prima di scrivere

Sviluppare senza pensare è la miglior strada per creare un pessimo codice.

Prima di sviluppare è necessario soffermarsi e pensare al contesto nel quale dobbiamo sviluppare gli endpoint, come organizzarle e focalizzarsi anche sul come farle evolvere: se tralasciamo questi aspetti ci ritroveremo a costruire una strada dissestata.

L’analisi delle api può seguire principalmente due approcci:

  • Bottom-up: Si parte dalle entità già esistenti e si costruiscono gli endpoint a partire da esse. I legami sono già note ed è l’approccio utilizzato nel caso si abbia un prodotto e si vuole creare un servizio di API REST generiche per clienti o integrazioni varie.
  • Top-Down: Si parte dall’analisi funzionale del contesto (es. mockup funzionalità o mockup UI) per poi arrivare alla costruzione delle API, in questo caso sono costruite ad hoc rispetto ad una soluzione nota. Con questo approccio si possono individuare ulteriori diramazioni come il BFF (backend-for-frontend, dove si creano delle api ad hoc per una UI predefinita), oppure un approccio contract-first (dove si creano i contratti API prima ancora di implementarle)

L’analisi non è una perdita di tempo, è un investimento nel futuro del prodotto. Spesso ho visto persone partire a sviluppare senza ragionare: vi posso assicurare che il risultato è stato pessimo.

Principi di design

Le risorse

Quando iniziamo a progettare le nostre API dobbiamo ragionare in termini di risorse e non di azioni. Le azioni sono identificate dai verbi (e lo vediamo dopo) e le risorse sono i moduli o i componenti della nostra applicazione.

Se dovessimo scrivere delle API di integrazione per R2-D2 allora i messaggi, le risorse, la catapulta della spada laser, il radar… sono tutte risorse del suo sistema operativo interno.

In una generica applicazione web le risorse potrebbero essere gli utenti, i contratti di lavoro, gli ordini, le vetture, ecc.

Le azioni (verbi)

I verbi definiscono parte dello scopo di un endpoint e ogni endpoint deve averne uno. I verbi che possiamo utilizzare sono:

  • GET: per recuperare una risorsa (es. ritorna l’elenco dei messaggi)
  • POST: per creare una nuova risorsa o per attivare un’azione (es. lancia la spada laser)
  • PUT: per sostituire/aggiornare completamente una risorsa esistente (es. aggiorna molte informazioni di un messaggio )
  • PATCH: per aggiornare parzialmente una risorsa esistente (es. per aggiornare solo il destinatario di un messaggio)
  • DELETE: per eliminare una risorsa. (es. elimina un messaggio specifico)

Questi sono alcuni esempi di API:

[GET] /users/123/details[GET] /users/123
[POST] /users/123/delete[DELETE] /users/123
[POST] /users/123/update-email[PATCH] /users/123

Formattazione endpoint

Per mantenere puliti, in ordine e standard gli endpoint ci sono alcune regole che dobbiamo seguire:

I nomi delle risorse vanno sempre espressi al plurale

❌​ /user

/users

No case-sensitive: progettiamo sempre i nostri endpoint in minuscolo

❌​ /Users

/users

Per accedere ad una specifica risorsa bisogna passare l’id subito dopo la risorsa.

/users/123/contracts

/users/123/contracts/58

Parole composte: negli endpoint ogni parola va separata con il simbolo meno (no underscore! E neanche il camel case, poiché violerebbe la prima regola)

❌​ /userContracts

❌​ /user_Contracts

/users-contracts

In funzione della dipendenza si parte dall’entità padre per poi proseguire con le entità direttamente correlate

Prendiamo il caso in cui esistano le seguenti entità: Utenti, contratti e contratti degli utenti

Nel momento in cui andremo a progettare le api avremo questi ipotetici endpoint:

/api/users Questo endpoint si riferisce agli utenti
/api/contracts Questo endpoint si riferisce ai contratti di lavoro generici, che un utente può sottoscrivere
/api/users/{id}/contracts Questo endpoint si riferisce ai contratti sottoscritti da un utente. Da notare come il contratto sia un’entità correlata ad un utente
/api/users/{id}/contracts/{id}/documentsDocumenti di uno specifico contratto di un utente

Filtri, ordinamenti e paginazione

Questo punto merita uno spazio tutto per sé. Ipotizziamo che un nostro endpoint di ricerca abbia 8 parametri di ricerca con il quale filtrare gli elementi, secondo quanto scritto fino adesso dovrei andare a gestire l’endpoint così :

[GET] /users/{filter1}/{filter2}/{filter3}/{filter4}/{filter5}/{filter6}/{filter7}/{filter8}

E se avessi solamente un filtro di ricerca? Oppure 2? È chiaro che questa non sia la strada corretta. Quando andiamo a gestire i parametri di ricerca dobbiamo andare ad utilizzare i parametri in querystring poiché essi sono opzionali (indico solamente quelli che sono stati passati) e non cambiano la struttura del mio endpoint.

Dal punto di vista evolutivo ci permette di mantenere lo stesso endpoint (/users ), aggiungendo nuovi filtri man mano che il software cresce.

[GET] /users?filterName=Giorgio&filterIsActive=true

Parliamo ora di paginazione: non è mai una best practice gestire un endpoint di ricerca senza gestire un ordinamento per due motivi:

  1. Il software evolve, oggi la mia lista ha solamente 10 elementi, domani 10mila
  2. Senza la paginazione obbligo sia il frontend che il backend a gestire molti dati, causando problemi di performance.

I parametri di paginazione e dimensione della pagina diventano quindi due parametri da aggiungere in querystring al nostro endpoint:

[GET] /users?filterName=Giorgio&filterIsActive=true&pageNumber=2&pageSize=30

Se adottiamo questa strategia su tutte le api avremo un modo standard per andare a gestire gli endpoint che ritornano delle collezioni di elementi. Non è solamente un discorso di pulizia e organizzazione del codice ma forniamo anche all’utente un modo univoco di approcciare questa tipologia di endpoint.

Queste volte mi è capitato di interfacciarmi con sistemi le cui API gestivano le collezioni in modi differenti? A volte erano metodi in POST, altri avevano i filtri in querystring, altre ancora non li avevano affatto: tutto questo genera confusione.

Per concludere questa parte aggiungo un ultimo piccolo tassello: l’oggetto di risposta. Nel caso delle paginazioni è bene che le api rispondano sempre con un oggetto standard, per gli stessi principi che abbiamo appena visto. Un esempio potrebbe essere il seguente:

Response
{
  "items": [],
  "totalItems": 0,
  "page": 0,
  "pageSize": 0,
  "totalPages": 0
}

Con questo oggetto un frontend ha tutte le informazioni necessarie per gestire un componente con la paginazione, capire se sono alla prima o all’ultima pagina e conoscere la quantità di elementi in totale.

Ricordiamoci che stiamo scrivendo un modo per permettere a qualcuno di accedere al nostro sistema e se non utilizziamo un approccio “standard” stiamo solamente costruendo un sistema caotico… e il mondo ne è già pieno!

Versioning

Se le API scritte per il nostro prodotto non subissero mai delle modifiche allora probabilmente gli affari non andrebbero molto bene! 🙂

Scherzi a parte, un sistema si aggiorna e si evolve… e le nostre api? Ovviamente anche loro devono adattarsi e per farlo dobbiamo indicare la versione che stiamo utilizzando.

Per indicarlo esistono principalmente due approcci:

  • Indicare la versione nell’url
  • Indicare la versione come chiave nell’header

Nella mia esperienza ho utilizzato entrambi gli approcci ma quella che più si utilizza e che preferisco è il primo approccio, ossia quello di indicare la versione nell’endpont, poichè risulta più chiaro e leggibile.

[GET] /v1/users

[GET] /v1.2/users

[GET] /v2/users

Il numero del versioning indica inoltre se ci sono dei breaking changes in merito ai parametri da passare o agli oggetti passati/ritornati. Se abbiamo un’api alla versione 1 e introduciamo un cambiamento nella logica di ritorno dei dati allora andremo a creare una versione 1.1, ma se il cambiamento porta una modifica all’oggetto ritornato (magari un campo rimosso o una nuova struttura) allora dovremo andare a creare una versione 2.

Mantenere molte versioni delle API significa però gestire la compatibilità anche lato backend. Quante versioni mantenere è una scelta che dipende dall’ambiente e dal contesto, in linea di principio si tende a dismettere le vecchie versioni di API (avvisando in largo anticipo i clienti, ovviamente!), per mantenere pulito il backend eliminando ciò che non serve più.

Ci sono contesti dove non è possibile eliminare vecchie API poiché ci sono dispositivi non aggiornabili per i quali dobbiamo mantenere la compatibilità (es. distributori automatici o vecchio hardware senza OTA): le evoluzioni delle api vanno studiate prima di partire e si può gestire l’obsolescenza dei campi rendendoli non obbligatori nelle nuove versioni. Ogni contesto va comunque analizzato e non esiste una risposta universalmente valida, ritorniamo al discorso iniziale sull’importanza della fase di analisi!

Codici di stato http

Qualcuno, prima di noi, ha inventato il web e con esso anche i codici di stato HTTP in modo tale che noi sviluppatori del futuro potessimo usarli… Ora alzi la mano chi utilizza regolarmente (e correttamente) questi codici.

Qui ho sintetizzato il significato del codice in base all’unità delle centinaia:

1xxLa richiesta è stata ricevuta e il processo continua. Poco usati nelle API
2xxLa richiesta è stata ricevuta, compresa e accettata con successo. Questo è ciò che vogliamo vedere!
3xxIl client deve intraprendere ulteriori azioni per completare la richiesta.
4xxLa richiesta contiene una sintassi errata o non può essere soddisfatta. L’errore è causato da chi ha chiamato l’API (es. non ho passato un parametro obbligatorio)
5xxIl server non è riuscito a soddisfare una richiesta apparentemente valida. L’errore è causato dal backend (es. database non raggiungibile o bug)

Scendendo più nel dettaglio di ogni singolo gruppo di errori, possiamo dire che quelli più comunemente utilizzati sono i seguenti:

CodiceSignificatoQuando usarlo
200OKLa richiesta è andata a buon fine. Risposta standard per GET. Può essere usata anche per PUT o PATCH se il corpo della risposta contiene la risorsa aggiornata.
201CreatedLa richiesta è andata a buon fine e una nuova risorsa è stata creata. Risposta a un POST che ha generato una nuova risorsa. La risposta deve includere l’header Location con l’URL della nuova risorsa.
204No ContentIl server ha elaborato con successo la richiesta, ma non restituisce alcun contenuto.         Risposta ideale per un DELETE andato a buon fine. Utile anche per PUT o PATCH quando non è necessario restituire la risorsa aggiornata.
CodiceSignificatoQuando usarlo
400Bad Request        Il server non può elaborare la richiesta a causa di un errore del client. Es: JSON malformato nel body, parametri di validazione non superati (es. un campo email non valido), parametri obbligatori mancanti.
401 Unauthorized        Qualcuno sta chiamando le nostre API senza un token di autenticazione (es. JWT), oppure è scaduto o non valido.
403Forbidden        Il client è autenticato ma non ha i permessi necessari per eseguire l’azione richiesta
404Not Found        Un grande classico. Se richiamassi l’endpoint /users/123 ma l’utente 123 non esistesse allora dovrei ritornare un 404.  
409Conflict        Stai probabilmente tentando di creare un qualcosa che esiste già (es. un utente con lo stesso username)
CodiceSignificatoQuando usarlo
500Internal Server Error        Si è verificato un errore generico e inaspettato sul server. E’ il caso di guardare i log
503Service Unavailable        Il server non è temporaneamente disponibile per gestire la richiesta. E’ il caso di chiamare a gran voce i sistemisti

Sicurezza

Nel corso della mia carriera ho scritto molte api (e molte alte le scriverò… spero!) e tutte quelle che erano senza autenticazione erano esposte all’interno di una rete aziendale o di un perimetro ben definito. Lasciare delle api senza sicurezza esposte in internet è come lasciare una finestra di casa sempre aperta: può sembrare “comodo” (non devi mai chiudere la finestra!) ma prima o poi qualcuno si intrufolerà dentro.

Su questo tema ci si potrebbe scrivere un libro intero ma rimanendo ad alto livello possiamo identificare 5 elementi principali

Autenticazione (AUTHeNthication)

Quando predisponiamo delle API (con autenticazione) dobbiamo preoccuparci di identificare un authentication provider (AuthN), uno strumento che si occupi di validare una chiave api o delle credenziali. Se la validazione va a buon fine ( http response 200! ) otterremo un token di autenticazione (tipicamente JWT) o una chiave API da includere nell’header “Authorization” delle nostre richieste.

Autorizzazione (AUTHoriZation)

L’autorizzazione (AuthZ) è uno step successivo all’autenticazione (AuthN) e risponde alla domanda “Cosa puoi fare?”: è il processo che determina che ruoli e permessi ha un utente in un sistema.

Se un’api di eliminazione utente richiede un permesso di “admin” e io ne sono sprovvisto, allora riceverò un errore 403.

Le strategie di autorizzazione possono essere raggruppate in 3 macro categorie:

  • Strategia RBAC (Role-Based-Access-List) – Strategia più comune, quello che puoi fare è determinato dai ruoli assegnati all’utente
  • Strategia ABAC (Attribute-Based-Access-List) – Più flessibile ma più complessa rispetto all’approccio RBAC, soprattutto in applicazioni enterprise dove la gestione dei permessi utente-risorsa può diventare complessa
  • Strategia ACL (Access-Control-List) – Ogni risorsa ha i sui permessi dedicati (es. Google Docs dove è possibile gestire permessi ad hoc per ogni documento)

In sistemi complessi è anche possibile trovare un mix tra questi approcci.

Ratelimit

Quando andiamo a progettare un’API dobbiamo tenere in considerazione il fatto che qualcuno potrebbe “affezionarsi” al nostro endpoint e richiamarlo in continuazione: questo causerebbe un rallentamento della nostra applicazione o innescherebbe meccanismi di scalabilità se siamo in ambiente cloud (con relativi costi annessi).

Per evitare tutto questo è bene andare a impostare un limite di richieste (al minuto o al secondo, dipende dalla funzione dell’endpoint), con un meccanismo di sospensione automatica dell’indirizzo IP.

Input validation

Sembra una banalità quasi scontata ma la validazione dell’oggetto di input va eseguita come prima cosa, in modo tale da evitare inutili elaborazioni che si bloccherebbero a metà processo (o peggio, che creino situazioni anomale).

Immagiamo di scrivere un endpoint che aggiorni l’email dell’utente: se non effettuiamo il controllo di validità dell’indirizzo fin da subito rischieremmo di aggiornare il dato e di ricevere un errore dal servizio di invio posta a causa dell’indirizzo errato.

CORS

Gli sviluppatori .NET che sono passati da .Net Framework a .NET Core si sono sicuramente scontrati con questo tema.

Quando parliamo di CORS (Cross-Origin-Resource-Sharing) parliamo di una strategia che permette di definire chi può richiamare le nostre API, andando a specificare quale dominio (o domini) accettiamo quando qualcuno richiama le nostre API Rest.

Le api sono stateless

I miei primi passi nel mondo web sono stati fatti con applicazioni AspNet (un giorno qualcuno mi chiederà “ma cosa era AspNet?”) e gestivo alcune informazioni nella sessione dell’utente (custodita dal caro e vecchio IIS): dopo aver effettuato il login potevo salvare delle informazioni di sessioni legate all’utente per poi usarle a piacimento dove mi pareva.

Poi è arrivato il cloud.

E con esso è nata l’esigenza della scalabilità

A sua volta si è consolidato il concetto di “stateless”, diventando la condizione base delle applicazioni web

Le API Rest non hanno nessuno stato utente in memoria, l’utente viene identificato tramite il token JWT (o altre strategie) e cosa può fare è decretato in funzione di questo.

Dietro le quinte

Idempotenza

La prima volta che ho sentito questa parola mi ero chiesto quale oscuro significato avesse, oggi invece è un concetto base da considerare nel momento in cui creo un endpoint (o che mi aspetto se richiamo un’api).

Un’endpoint è idempotente se il risultato non cambia anche a fronte di chiamate multiple.

Creare un’API idempotente in alcuni contesti è molto importante, non farlo significherebbe innescare dei processi multipli che potrebbero portare a perdita dei dati o comportamenti inaspettati.

Immaginiamo di sviluppare un endpoint per pagare online, utilizzato da un’applicazione Angular. Arriva il cliente e nel momento di pagare 100Euro la sua connessione vacilla.

Preme nuovamente il pulsante paga perché non succede nulla

Frustrazione

Clicca ancora ma non succede nulla.

Rabbia

Preme compulsivamente il tasto “Paga”

La connessione si riprende e tutti i 24 click fatti dall’utente vanno a buon fine. Cosa succederebbe se non avessi un endpoint che gestisca l’idempotenza? Risposta: anziché gestire un unico pagamento ne gestirei 24, creando disagio all’utente e abbassando reputazione della mia azienda.

Caching

Uno dei grandi vantaggi delle API Rest è che possiamo utilizzare meccanismi di caching per evitare di riprocessare delle richieste che abbiamo già elaborato poco prima. Non tutti i dati cambiano spesso, alcuni hanno un basso tasso di aggiornamento (per esempio il numero di satelliti che orbitano attorno alla terra o il numero di paesi nel mondo)

In contesti più enterprise il caching è gestito da un api gateway, in altri invece dobbiamo tenerne conto ed implementarlo noi stessi: tutti i maggiori framework offrono opzioni di gestione della cache.

I vantaggi di implementare una cache sono:

  • La risposta delle nostre API è più veloce (evito il calcolo o la lettura dei dati)
  • Evito di riprocessare una richiesta della quale ho già la risposta
  • In ambiente cloud non processare una richiesta inutile significa risparmio (e se le richieste sono migliaia il risparmio è garantito!)

Monitoraggio e osservabilità

Nel 2025 non è pensabile creare delle nuove API enterprise senza prevedere il monitoraggio o il log tracing. L’applicazione nel tempo cresce, gli endpoint si moltiplicano e ci ritroviamo a correre ai ripari perché non sappiamo quali endpoint sono più chiamati, quali sono ignorati e quali sono quelli che ritornano errore più spesso (true story).

Esistono molti strumenti, sia gratuiti che a pagamento, che hanno l’obiettivo di monitorare le nostre api, quali usare dipende dal contesto aziendale.

In passato ho usato sia Prometheus+Grafana che ELK, entrambi sistemi open source molto validi.

Documentazione

E’ l’ultimo punto di questo lungo articolo ma è anche uno dei più importanti. La documentazione è fondamentale sia per noi (quante volte sviluppiamo un qualcosa senza documentazione e dopo 1 mese non ci ricordiamo più cosa fa?) sia per il cliente (se un endpoint non è documentato come faccio a capire cosa fa?).

Di questo ne parlerò in un articolo dedicato, ma ho lavorato con un’azienda che aveva un proprio prodotto (30 anni di sviluppo) completamente senza documentazione. Il risultato? Venivano sviluppate le stesse funzionalità più e più volte perché nessuno si ricordava più che la funzionalità era già stata implementata.

Conclusioni

Al giorno d’oggi, tra AI e articoli vari, scrivere delle buone API è una scelta, non farlo significa condannare un servizio già in partenza accumulando inutile debito tecnico. Le API sono il nostro biglietto da visita, aziendale o personale, con le quali permettiamo di connetterci.

Spero che questo articolo sia stato per voi interessante, magari vi ha solo rispolverato delle conoscenze in un qualche cassetto della memoria, magari vi ha fatto scoprire qualcosa di nuovo.

Se pensate che sia stato utile inviatelo al collega più in difficoltà, magari potrà aiutarlo a crescere e chissà, magari vi arrabbierete meno a vedere certi endpoint!

Alla prossima!

Condividi questo articolo
Shareable URL
Post precedente

EF Core 10: .leftJoin() e .rightJoin() in LINQ

Prosimo post

Architettura e organizzazione di un’applicazione enterprise dinamica

Leggi il prossimo articolo