Leggendo qua e là documentazione sui servizi REST e sull'uso corretto dei metodi previsti da HTTP, mi sono accorto che spesso mancano alcune informazioni basilari, che permettono di comprendere a fondo i concetti esposti.
Innanzitutto, vi sono due tipi di servizi che possono essere offerti da un server web: quelli pensati per essere utilizzati da un utente umano, tramite il browser, e quelli pensati per essere utilizzati da applicazioni, che a loro volta saranno utilizzate da esseri umani.
Sempre più spesso, un server web offre entrambe le possibilità: nasce offrendo qualche servizio utilizzabili dai propri utenti tramite browser, e successivamente, quando gli utenti chiedono di automatizzare qualche processo, mette a disposizione dei metodi per l'uso degli stessi (o di altri) servizi da parte di applicazioni diverse.
(Scegliete un social network di vostro gradimento, e cercate nel relativo sito web la sezione dedicata agli sviluppatori per rendervene conto.)
Facciamo un esempio. L'ipotetico nuovo sito web kitchen.example.com consente ad una persona di controllare via web l'attrezzatura e le provviste della propria cucina, completamente automatizzata. Tutto questo via web, con un utente umano che usa un normale browser.
L'applicazione dovrà mettere a disposizione anche un'interfaccia per registrare i carichi di provviste in un database. Offrire all'utente una normale interfaccia web, da utilizzare con il proprio browser, per caricare questi dati potrebbe non essere un'idea ottimale (eppure, quante volte è capitato di vedere cose del genere!). Se avete provato a caricare manualmente dati di tipo analogo (insiemi di fotografie, di documenti vari, ecc.) in numeri maggiori a tre / quattro sapete di che cosa sto parlando. L'ideale è di fornire un modo per automatizzare il procedimento di caricamento delle provviste acquistate e di rendere pubbliche le specifiche tecniche relative a questo processo, in modo che, ad esempio, sia possibile sviluppare un'applicazione che carica le informazioni sul server a partire da qualche altra fonte di dati (ad esempio, il programma di controllo degli scontrini di acquisto...).
Se le specifiche rispettano alcune convenzioni, poi, tanto di guadagnato, perché un qualsiasi programmatore sarà in grado di adattare l'esperienza maturata in progetti precedenti al nuovo caso, senza dover inventare ogni volta la ruota.
Le specifiche tecniche sono documenti che spiegano in maniera dettagliata come un programma può invocare operazioni sul server, e vengono comunemente indicate come API (application program interface). Il server che risponde alle richieste di applicazioni sviluppate seguendo queste indicazioni viene a volte indicato come apiserver. L'applicazione che invia le richieste può essere di diversi tipi: uno script bash eseguito da riga di comando con all'interno richiami di cURL, un'applicazioncina web che sfrutta javascript per le richieste, un'applicazione desktop, un'applicazione per smartphone, un'applicazione web ordinaria, ecc.
Gli scenari naturalmente possono essere molto articolati. Potrebbe succedere che una persona interagisca tramite browser con un webserver che a sua volta si basa su un apiserver per elaborare la propria risposta.
Le app degli smartphone spesso sono programmi che sfruttano le API di un servizio, quindi senza rendervene conto avete sfruttato queste cose molte volte nella vostra esperienza di utenti.
Prima di analizzare le differenze che esistono fra applicazioni web ordinarie e applicazioni che definiscono delle API per l'interazione, e di introdurre il discorso di REST, facciamo un piccolo veloce riepilogo di come funziona HTTP. Il web abbonda di esempi al riguardo, per cui non sarò molto dettagliato.
Il client HTTP (user-agent, può essere un browser o qualsiasi altro programma in grado di fare richieste e ricevere risposte) invia al server HTTP (webserver o apiserver) richieste con un metodo, un percorso, la versione del protocollo usato e una serie di intestazioni. Ad esempio:
GET /kitchens/1/cookers/4.html HTTP/1.1
Host: kitchen.example.com
User-Agent: simpleBrowser v.1
La risposta del server potrebbe essere una pagina web che dà informazioni sullo stato del fornello 4 della cucina 1.
In questo caso, il metodo è GET, la versione di HTTP è 1.1, il percorso è /kitchen/1/cooker/4.html. L'URI completa è http(s)://kitchen.example.com/kitchen/1/cooker/4.html, ma viene suddivisa in due parti per motivi storici, legati al fatto che originariamente un server web poteva ospitare un solo dominio, per cui era di fatto inutile specificare nella richiesta a quale host ci si voleva rivolgere.
(Non fatevi fuorviare dal fatto che ci sia quel .html alla fine del percorso; le applicazioni moderne usano un modulo chiamato URL-rewriting che consente di mappare tutte le richieste verso specifici moduli applicativi, che possono essere scritti in php, perl, python, ruby o ciò che volete, e l'estensione viene usata per determinare il formato con cui si vuole ottenere la rappresentazione -- se avete sviluppato un'applicazione in php e vi hanno detto che il path deve finire per .php, sappiate che non è effettivamente così: nella configurazione tipica, è il file che deve essere eseguito sul server a dover avere l'estensione .php, e si tratta di una cosa diversa.)
I metodi principali per le richieste sono GET (per ottenere dati), POST (per aggiungere dati), PUT (per aggiornare dati già esistenti, o per aggiungerni di nuovi in una posizione determinata dal client e non dal server) e DELETE (per eliminare dati). L'RFC 5789 ha introdotto anche il metodo PATCH (per aggiornare parte dei dati esistenti relativi a una risorsa).
Ciascuno metodo può possedere o meno le seguenti due caratteristiche:
- sicurezza (l'esecuzione della richiesta non deve avere effetti collaterali, ossia non deve modificare le informazioni sostanziali sul server, escludendo operazioni di log, incremento di contatori, ecc.);
- idempotenza (richieste ripetute identiche devono portare al medesimo stato dei dati sul server).
metodo | sicuro | idempotente |
---|---|---|
GET | sì | sì |
PUT | no | sì |
DELETE | no | sì |
PATCH | no | no |
POST | no | no |
I metodi sicuri, come GET, devono poter essere eseguiti senza correre il rischio di causare effetti sul server. Ad esempio, un browser potrebbe precaricare la pagina successiva di una serie di pagine anche senza che l'utente umano faccia clic sul link corrispondente, al fine di velocizzare l'esperienza di navigazione. Oppure un programma di mirroring deve poter seguire tutti i link di una pagina per creare una copia locale del sito remoto senza causare nessun cambiamento di stato.
(Se non avete mai provato a fare il mirroring di un sito web, potete farlo con strumenti semplici come wget, che dispone dell'apposita opzione.)
In contrasto, i metodi non sicuri, come PUT, DELETE e POST, cambiano lo stato delle informazioni sul server. Ad esempio, un'ipotetica richiesta tipo
DELETE /kitchens/2/sauces/1234 HTTP/1.1
Host: kitchen.example.com
User-Agent: simpleBrowser v.1
dovrebbe portare alla cancellazione della salsa con codice 1234 dal database delle salse legato alla cucina 2. È bene quindi che tali metodi vengano eseguiti solo quando un utente umano effettivamente vuole usarli.
I metodi idempotenti sono quelli che, anche se eseguiti ripetutamente con la stessa richiesta, portano allo stesso risultato in termini di stato del server (anche se la risposta potrebbe essere diversa). Tornando all'esempio precedente, alla prima richiesta di cancellazione il server potrebbe rispondere che la cancellazione è avvenuta, e ad una seconda richiesta che la risorsa da cancellare non esiste. In entrambi i casi, comunque, lo stato finale sarà che la salsa con id 1234 non esisterà.
Similmente, l'accensione del fuoco del fornello 4 della cucina 2 potrebbe essere fatto con una richiesta del tipo
PUT /kitchens/2/cookers/4 HTTP/1.1
Host: kitchen.example.com
User-Agent: simpleBrowser v.1
status=on
mentre lo spegnimento potrebbe avvenire con
PUT /kitchens/2/cookers/4 HTTP/1.1
Host: kitchen.example.com
User-Agent: simpleBrowser v.1
status=off
Il metodo POST, che non è idempotente, serve a causare ogni volta l'aggiunta di informazioni sul server. Quindi, ad esempio, tre richieste ripetute di tipo
POST /kitchens/2/sauces HTTP/1.1
Host: kitchen.example.com
User-Agent: simpleBrowser v.1
type=tomato&price=1.12&quantity=20&mu=l
dovrebbero portare alla registrazione del carico di 60 litri di salsa di pomodoro.
Notate che con il metodo POST non si dovrebbe indicare l'id della risorsa, perché ne stiamo chiedendo al server di aggiungerla, e sarà esso (o, più concretamente, il dbms) a determinare l'id.
Nella risposta ad una richiesta di tipo POST, il server dovrebbe fornire l'URI della risorsa creata, in modo da consentire eventuali modifiche successive, che potranno essere fatte tramite PUT, se si vogliono sostituire tutte le informazioni presenti, oppure tramite PATCH, se si vogliono modificare solo degli attributi.
Un esempio di richiesta di modifica dell'attributo prezzo potrebbe essere la seguente:
PATCH /kitchens/2/sauces/8 HTTP/1.1
Host: kitchen.example.com
User-Agent: simpleBrowser v.1
price=1.14
L'uso del browser pone dei limiti rispetto a quanto consentito, in generale, dall'HTTP. Il limite maggiore è che l'HTML (non l'HTTP) supporta solo i metodi GET e POST (per vari motivi, che non discuteremo in questa sede). Quando fate clic su un link, il browser fa una richiesta di tipo GET. Quando compilate una form e inviate i dati, il metodo di invio dipende dall'attributo "method" dell'elemento form. Ma non si possono avere form con attributo "method" impostato a DELETE o PUT. Di conseguenza, quando si sviluppa un'applicazione web pensata per essere eseguita da un utente umano tramite browser, il tipo di richiesta potrà essere solo GET o POST: il primo verrà usato quando si devono mostrare informazioni (lista dei prodotti, scheda del prodotto, form per la modifica dei dati di un prodotto, ecc.), il secondo quando dei dati devono essere effettivamente acquisiti (inserimento di un nuovo prodotto, cancellazione di un prodotto, modifica di un prodotto, ecc.).
Immaginando di dover presentare un link per la cancellazione di una risorsa, la soluzione migliore da adottare, in un'ottica di miglioramento progressivo, sarebbe di:
- implementare la soluzione con un link ordinario ad una pagina di richiesta della cancellazione in cui viene presentata una form con un pulsante per la cancellazione, che userà il metodo POST per l'invio dei dati;
- sul server, fare in modo che la richiesta fallisca se il metodo usato non è POST;
- utilizzare codice javascript discreto per sostituire il link ordinario con un link che faccia direttamente il POST dei dati, presentando una finestra di conferma di cancellazione;
- eventualmente, aggiungere codice AJAX da attivare su richiesta (javascript può fare richieste asincrone che usano i metodi PUT e DELETE).
Per fare delle prove, è possibile usare cURL dalla riga di comando. Ad esempio:
curl -X PUT -d status=off http://kitchen.example.com/kitchens/2/cookers/4
consente di fare la richiesta di spegnimento del fuoco del fornello numero 4 della seconda cucina, come visto precedentemente.
Veniamo ora al concetto di API REST. Innanzitutto, che cos'è REST?
Si tratta di un'architettura software per la gestione di risorse, basata su principi che delineano come le risorse devono essere definite e indirizzate. La sigla sta per "Representational State Transfer", ed è stata coniata da Roy Fielding. Informazioni dettagliate e ulteriori link si possono trovare nella pagina della Wikipedia. In linea teorica REST potrebbe essere usato anche senza HTTP, ma in pratica HTTP e REST viaggiano quasi sempre in coppia, per cui darò per scontato il fatto che si usi HTTP (spesso nella versione sicura, HTTPS). I concetti fondamentali sono questi:
- visto che HTTP prevede già l'uso di metodi specifici per le diverse operazioni, ci si concentra sulla risorsa piuttosto che sul cosa deve essere fatto, per la definizione degli URI, in cui si vedranno riferimenti alle cose, non alle azioni;
tradotto: nel caso di API per un negozio online, nell'URI non si dovrebbero vedere "verbi" tipo "buy", "pay", ecc., ma solo riferimenti alle risorse effettive; per un pagamento pianificheremo un URI come http(s)://example.com/payment/transaction/1234/amount/150, da richiamare con il verbo POST per effettuarlo - le risorse sono identificate da URI, e diversi URI possono puntare alla stessa risorsa;
tradotto: la stessa notizia potrebbe essere identificata dall'URI http(s)://example.com/news/2013/10/22/italy/web sia dall'URI http(s)://example.com/news/italy/web/latest (in un dato momento) - una risorsa è diversa dalla sua rappresentazione (posso rappresentare i dati della salsa 1234 in un file XML, in un file JSON, con un'immagine, in una pagina HTML) -- sarà lo user-agent a chiedere quale tipo di rappresentazione gli interessa;
tradotto: la notizia del 22 ottobre identificata dall'URI http(s)://example.com/news/2013/10/22/italy/web potrebbe essere fornita dal server in formato HTML, JSON, XML, ecc., a seconda di ciò che richiede il browser -- su alcune opzioni disponibili ho scritto un altro post in questo blog - il formato con cui il client specifica come vuole ricevere i dati, secondo HTTP, dovrebbe essere specificato nelle intestazioni, in un campo di "negoziazione del contenuto", ma a fini pratici spesso si usa semplicemente l'estensione;
tradotto: quando uno user agent richiede una risorsa via HTTP, può specificare nella richiesta il formato con cui desidera ottenere le informazioni (es. Accept: text/plain), ma spesso nelle implementazioni si dà la possibilità di usare l'estensione (es. http(s)://example.com/news/2013/10/22/italy/web.txt) - l'interfaccia è uniforme, ossia le cose si fanno sempre allo stesso modo indipendentemente dal tipo di risorsa (questo vale sia per il metodo HTTP da usare, sia per il tipo di risposta che si ottiene dal server);
tradotto: per eliminare una risorsa si usa sempre il metodo DELETE, ecc. - la risposta del server contiene un codice HTTP e, spesso ma non sempre, un payload (ossia le informazioni richieste);
tradotto: il server risponde con un codice tipo 200 OK, oppure 404 File not found, ecc; per alcune risposte potrebbero non esserci altre informazioni da rappresentare (contenuti HTML, testi, file binari, ecc.) - un'applicazione REST dovrebbe essere completamente senza gestione di stato da parte del server (quando l'utente interagisce con un server tramite browser, invece, la sessione viene gestita con una collaborazione tra client e server, che generalmente avviene con l'impostazione di cookies di sessione al momento dell'autenticazione; nelle applicazioni REST il server non dovrebbe mantenere informazioni sulla "sessione", e ogni richiesta dovrebbe essere valutata autonomamente rispetto a quelle precedenti e a quelle successive) -- ciò consente di gestire, ad esempio, il bilanciamento di carico tra più server;
tradotto: ogni richiesta inviata al server dal client dovrebbe essere in qualche modo indipendente dalle precedenti o, meglio, non deve essere il server a farsi carico di tenere traccia di chi è autorizzato a fare qualche cosa, come nel caso delle sessioni che con un browser vengono avviate dall'utente con il login - le rappresentazioni possono essere memorizzate temporaneamente in una cache, anche a più livelli (come quando tra client e server si frappongono dei proxy);
tradotto: quando un client invia la richiesta ad un server, può avvalersi di server intermediari (proxy), i quali sono autorizzati a memorizzare la risposta del server per un determinato lasso di tempo, in modo da fornirla ad eventuali altri client che la dovessero richiedere - le rappresentazioni fornite dal server dovrebbero contenere collegamenti ipertestuali (URI) che consentono di passare da uno stato all'altro (questo concetto viene indicato come HATEOAS, Hypermedia as the engine of application state, ma è uno dei principi più violati dell'architettura, come spiegato nel video HATEOAS 101);
- quando client e server devono comunicare tra loro, è bene che i dati vengano trasmessi utilizzando formati facilmente gestibili da sistemi automatizzati, come JSON e XML (per quanto riguarda JSON, esistono anche delle proposte di standard di fatto, come JSend, in cui si stabilisce come la risposta del server debba contenere dati relativi all'esito della richiesta) -- notate che gli esempi presentati qui sopra sono semplificati in quanto non usano JSON.
Per molti servizi è disponibile abbondante documentazione sulle API REST che si possono utilizzare, ed è sempre una buona idea dare un'occhiata per ottenere ispirazione sulle pratiche correnti. Inoltre, per approfondimenti, consiglio la lettura di due raccolte di buone pratiche: RESTful Best Practices e Best Practices for Designing a Pragmatic RESTful API.