Block Image

Architettura REST

L'architettura REST (Representational State Transfer) oggigiorno è ampliamente usata nei backend, sostituendo man mano l'uso di SOAP.

A differenza di SOAP, REST è appunto uno stile architetturale e non un protocollo; quindi dà delle linee guida su come sviluppare API, senza specificare i protocolli da usare. Solitamente chi usa questo stile architetturale, si avvale del protocollo HTTP per far comunicare il client col server, usa il formato JSON per ricevere/mandare dati. Ma l'uso di questi protocolli non costituisce assolutamente un vincolo da parte dell'architettura REST.

In questo articolo non spiegheremo cos'è nel dettaglio REST, quali sono i suoi vincoli e quali sono le differenza con SOAP; piuttosto, andremo a vedere quali sono le best practices per scrivere un buon Web Service RESTful utilizzando le tecniche e la suddivisione in livelli di Leonard Richardson.

Il Richardson Maturity Model

Il Richardson Maturity Model è un modello che dà delle linee guida per la creazione di servizi REST, consiste in 4 livelli (da 0 a 3) e il livello 3 rispetta appieno come dovrebbe essere un vero Web Service RESTful. Ma quante volte abbiamo visto a lavoro dei servizi REST rispecchiare le best practices del livello 3? Forse mai. Perchè non vengono rispettate? Un po' perchè non si conoscono (spesso), un po' perchè è piu complesso creare servizi che li rispettano.

Level 0: The Swamp of POX

Non esiste il concetto di risorsa, non esiste il concetto di metodi HTTP. I servizi hanno una singola URI e fanno uso di un singolo metodo HTTP (solitamente POST). Praticamente l'HTTP viene utilizzato solo come protocollo di trasporto.
Questo meccanismo viene utilizzato ad esempio nelle applicazioni POX (Plain Old XML) e nei servizi SOAP.
Esempio: voglio le informazioni dello user con id 1:

Request url: http://localhost:8080/api (POST)
Request body:

<UserRequest>
    <id>1</id>
</UserRequest>

Response (200 OK):

<User>
    <id>1</id>
    <name>Vincenzo</name>
    <surname>Racca</surname>
</User>

Se lo user con id 1 non esiste:

Response (200 OK):

<UserRequestError>
    <id>1</id>
    <reason>The user doesn't exist</reason>
</UserRequestError>
Nota 1: L'esempio poteva essere fatto anche usando JSON come formato della richiesta e della risposta. Si è scelto di usare l'XML in questo caso poichè il livello 0 viene usato spesso per applicazioni che usano l'XML.

Level 1: Resources

In questo livello c'è il concetto di risorsa. Ogni risorsa sarà identificata da una specifica URI. Non esiste ancora il concetto di verbo HTTP.

Uguale all'esempio precedente, ma l'url della richiesta è:
http://localhost:8080/api/users (POST)

Level 2: HTTP verbs

Qui entra in gioco il concetto di verbi HTTP, che possiamo riassumere brevemente:

  1. richieste GET per recuperare la risorsa, con http response 200 se esiste, 204 se la collezione è vuota, 404 se la singola risorsa non esiste
  2. richieste POST per creare una nuova risorsa, con response 201 se è stata create correttamente
  3. richieste PUT per modificare una risorsa se esiste, crearla altrimenti
  4. richieste DELETE per eliminare (non importa se fisicamente o logicamente) una risorsa

Nell'esempio precedente, abbiamo usato il verbo POST per richiedere le informazioni dell'utente 1. Usando un servizio REST di livello 2, la richiesta diventa:
http://localhost:8080/api/users/1 (GET, senza request body)

Response (200 OK se la risorsa esiste):

{
  "id": "1",
  "name": "Vincenzo",
  "surname": "Racca",
  "cars": [
     {
        "id":1,
        "plate":"BG929RF"
     },
     {
        "id":2,
        "plate":"AG929RF"
     }
  ]
}

Response (404 NOT FOUND se la risorsa non esiste).

Il verbo GET è definito come metodo sicuro, perchè viene utlizzato solo per recuperare dati. Questo lo rende anche idempotente.

«Una chiamata REST è definita idempotente se un client può effettuare una stessa chiamata ripetuta più volte producendo sempre lo stesso risultato.»

Anche i metodi PUT e DELETE sono definiti idempotenti. Chiamando più volte la PUT con la stessa URI e la stessa request, sovrascriverai sempre la risorsa e otterrai sempre lo stesso risultato. Chiamando più volte la delete con la stessa URI elimini la risorsa, se esiste, altrimenti non effettui nessuna operazione. Effettuando n chiamate POST sulla stessa URI invece, crei potenzialmente n risorse nuove.

Level 3: Hypermedia Controls

Il livello finale introduce l'HATEOAS (Hypermedia As The Engine Of the Application State). In pratica ogni risorsa contiene dei link a tutte le risorse collegate e non solo. Questo permette innanzitutto di poter dire al client quali altre risorse sono collegate alla risorsa chiamata, senza però appesantire il corpo della risposta in quanto questi collegamenti sono semplici link. Un ulteriore vantaggio è che il client può esplorare le chiamate messe a disposizione dal server senza una vera documentazione sulla lista di tutte le API disponibili. Inoltre lato server si possono facilmente visualizzare nuove relazioni con altre risorse aggiungendo semplicemente link.

Request: http://localhost:8080/api/users/1 (GET, senza request body)

Response (200 OK se la risorsa esiste):

{
     "id": 1,
     "name": "Vincenzo",
     "surname": "Racca",
     "_links": {
         "cars": {
             "href": "http://localhost:8080/api/users/1/cars"
         },
         "self": {
             "href": "http://localhost:8080/api/users/1"
         }
     }
 }

Da notare che ora cars è un semplice link che il client potrà invocare per recuperare tutte le cars dell'utente 1.
Particolarmente utile è anche la paginazione con HATEOAS:

Request: http://localhost:8080/api/users?page=0&size=2

Response:

{
    "_embedded": {
        "users": [
            {
                "id": 1,
                "name": "Vincenzo",
                "surname": "Racca",
                "_links": {
                    "cars": {
                        "href": "http://localhost:8080/api/users/1/cars"
                    },
                    "self": {
                        "href": "http://localhost:8080/api/users/1"
                    }
                }
            },
            {
                "id": 2,
                "name": "Pippo",
                "surname": "Pluto",
                "_links": {
                    "cars": {
                        "href": "http://localhost:8080/api/users/2/cars"
                    },
                    "self": {
                        "href": "http://localhost:8080/api/users/2"
                    }
                }
            }
        ]
    },
    "_links": {
        "first": {
            "href": "http://localhost:8080/api/users?page=0&size=2"
        },
        "self": {
            "href": "http://localhost:8080/api/users?page=0&size=2"
        },
        "next": {
            "href": "http://localhost:8080/api/users?page=1&size=2"
        },
        "last": {
            "href": "http://localhost:8080/api/users?page=2&size=2"
        }
    },
    "page": {
        "size": 2,
        "totalElements": 5,
        "totalPages": 3,
        "number": 0
    }
}

Abbiamo nella response anche i link per andare alla prossima pagina, alla prima e all'ultima. Comodo no?

Conclusioni

Abbiamo visto cos'è il Richardson Maturity Model e quindi i vari livelli REST. Nel prossimo articolo creeremo un Web Service RESTful di livello 3 senza usare Spring Data Rest, in modo tale da personalizzarlo come vogliamo. Utilizzeremo la libreria Spring HATEOAS.