Salta ai contenuti

Architettura REST

Overview Image

REST è l’acronimo di REpresentational State Transfer ed è uno stile architetturale per sistemi distribuiti basati su ipermedia. Roy Fielding lo ha presentato nel 2000 nella sua nota dissertazione. Da allora è diventato uno degli approcci più diffusi per la costruzione di API web (Application Programming Interfaces).

REST non è un protocollo né uno standard; è uno stile architetturale. Durante lo sviluppo, i progettisti di API possono implementare REST in molteplici modi. Come ogni stile architetturale, REST definisce principi guida e vincoli. Tali principi devono essere soddisfatti affinché un’interfaccia di servizio possa essere definita RESTful. Una Web API (o Web Service) conforme allo stile architetturale REST è chiamata REST API (o RESTful API). REST dunque definisce un insieme di principi architetturali per la progettazione di un sistema. Rappresenta uno stile, dunque non si riferisce a un sistema concreto e normato da un ente di standardizzazione.

La definizione compare per la prima volta nel 2000 nella tesi di Roy Fielding, “Architectural Styles and the Design of Network-based Software Architectures”, presentata presso l’Università della California a Irvine. In essa si analizzano i principi alla base di diverse architetture software, inclusi quelli che consentono di considerare il Web come una piattaforma per l’elaborazione distribuita.

È opportuno precisare che i principi REST non sono necessariamente legati al Web; si tratta di principi astratti di cui il World Wide Web è un esempio concreto.

REST prevede che la scalabilità del Web e la sua crescita siano il risultato di alcuni principi chiave di progettazione:

  • Lo stato dell’applicazione e le funzionalità sono suddivisi in risorse web.
  • Ogni risorsa è univoca e indirizzabile tramite una sintassi universale, usata nei collegamenti ipertestuali.
  • Tutte le risorse condividono un’interfaccia uniforme per il trasferimento di stato tra client e risorse, che include:
    • un insieme vincolato di operazioni ben definite;
    • un insieme vincolato di contenuti, opzionalmente supportato da codice a richiesta;
    • un protocollo che è: client-server, privo di stato (stateless), cacheable e a livelli (layered).

In breve, REST non è un’architettura né uno standard, ma un insieme di linee guida per la realizzazione di un’architettura di sistema. I principi che rendono il Web adatto alla realizzazione di Web Service secondo l’approccio REST possono essere sintetizzati nei seguenti punti:

  • Identificazione delle risorse
  • Utilizzo esplicito dei metodi HTTP
  • Risorse autodescrittive
  • Collegamenti tra risorse
  • Comunicazione senza stato

L’approccio architetturale REST è definito dai seguenti vincoli (l’implementazione dei singoli componenti resta libera):

  • Client–server: un insieme di interfacce uniformi separa client e server. Il client non si occupa della persistenza; i server non gestiscono UI o stato utente. Ciò favorisce portabilità e scalabilità.
  • Stateless: la comunicazione client–server è priva di stato lato server. Ogni richiesta contiene tutte le informazioni necessarie. Lo stato di sessione risiede nel client (o in un archivio esterno esplicito).
  • Cacheable: le risposte indicano esplicitamente se sono cacheable. Una corretta gestione della cache riduce le comunicazioni migliorando scalabilità e prestazioni.
  • Layered system: l’architettura può essere organizzata a strati (API gateway, backend dati, servizi di autenticazione, ecc.). Il client non deve conoscere la topologia interna.
  • Code on demand (opzionale): il server può estendere temporaneamente le funzionalità del client trasferendo codice eseguibile (es. script JavaScript).
  • Uniform interface: un’interfaccia omogenea tra client e server semplifica e disaccoppia l’architettura.

Elemento centrale in REST è la risorsa, accessibile tramite un identificatore globale (URI). Le componenti della rete (client e server) comunicano attraverso un’interfaccia standard (es. HTTP) per scambiarsi rappresentazioni di tali risorse (il documento che trasmette le informazioni). Una medesima risorsa può avere più rappresentazioni (per esempio SVG o CSV nel caso di un cerchio), selezionate tramite negoziazione di contenuto.

La natura stateless comporta che ciascun connettore (client, server, cache, tunnel) intervenga sulle singole richieste senza memoria delle precedenti: per interagire con una risorsa servono l’identificatore della risorsa e l’azione richiesta. Il formato della rappresentazione (HTML, XML, JSON, immagini, ecc.) deve essere noto al client.

Resource Naming

Nello stile REST, la rappresentazione primaria dei dati è la risorsa. Una strategia coerente e robusta di denominazione delle risorse è una delle decisioni progettuali più importanti nel lungo periodo.

Secondo Fielding, qualunque informazione nominabile può essere una risorsa: un documento o un’immagine, un servizio temporale (ad es. “meteo di oggi a Los Angeles”), una collezione di risorse, un oggetto non virtuale (ad es. una persona), e così via. Una risorsa è una mappatura concettuale su un insieme di entità, non l’entità concreta in un dato istante.

Una risorsa può essere una singola entità (singleton) oppure una collezione. Esempio in dominio bancario: “customers” è una collezione e “customer” è un singleton. Le rispettive URI possono essere, ad esempio, “/customers” e “/customers/{customerId}”.

Una risorsa può contenere sub-collection. Ad esempio, gli “accounts” di un particolare cliente: /customers/{customerId}/accounts; il singolo “account” nella sub-collection: /customers/{customerId}/accounts/{accountId}.

Le REST API usano gli URI per indirizzare le risorse. È buona pratica progettare URI che riflettano il modello delle risorse esposto ai client. Quando le risorse sono denominate bene, l’API risulta più intuitiva.

Le URI RESTful dovrebbero riferirsi a “cose” (sostantivi) e non ad azioni (verbi). Alcuni esempi: utenti del sistema, account utente, dispositivi di rete. Esempi di URI:

http://api.example.com/device-management/managed-devices
http://api.example.com/device-management/managed-devices/{device-id}
http://api.example.com/user-management/users
http://api.example.com/user-management/users/{id}

Per chiarezza, si distinguano i seguenti archetipi di risorsa e si adotti con coerenza la relativa convenzione di naming:

Una risorsa documento rappresenta un concetto singolare, analogo a un record o a un’istanza di oggetto. È tipicamente un elemento all’interno di una collezione e la sua rappresentazione include campi e link ad altre risorse. Usare nomi al singolare:

http://api.example.com/device-management/managed-devices/{device-id}
http://api.example.com/user-management/users/{id}
http://api.example.com/user-management/users/admin

Una collezione è una directory di risorse gestita dal server. Il client può proporre nuove risorse, ma è la collezione che decide se e come crearle e con quali URI. Usare nomi al plurale:

http://api.example.com/device-management/managed-devices
http://api.example.com/user-management/users
http://api.example.com/user-management/users/{id}/accounts

Uno store è un repository gestito dal client: consente di inserire, recuperare e cancellare risorse. Le URI sono scelte dal client al momento dell’inserimento. Usare nomi al plurale:

http://api.example.com/song-management/users/{id}/playlists

Un controller modella un concetto procedurale (funzione eseguibile, con parametri e ritorni). Usare verbi:

http://api.example.com/cart-management/users/{id}/cart/checkout
http://api.example.com/song-management/users/{id}/playlist/play

Adottare convenzioni coerenti per denominazione e formattazione delle URI. Suggerimenti:

http://api.example.com/device-management
http://api.example.com/device-management/managed-devices
http://api.example.com/device-management/managed-devices/{id}
http://api.example.com/device-management/managed-devices/{id}/scripts
http://api.example.com/device-management/managed-devices/{id}/scripts/{id}

Siccome lo slash finale nel percorso non aggiunge semantica e può confondere è meglio ometterlo.

http://api.example.com/device-management/managed-devices/
http://api.example.com/device-management/managed-devices
/*Questa versione è molto migliore*/

I trattini migliorano la leggibilità dei segmenti lunghi del percorso.

http://api.example.com/device-management/managed-devices/
http://api.example.com/device-management/managed-devices
/*Questa versione è molto migliore*/

Gli underscore possono risultare poco visibili a seconda dei font. Preferire i trattini.

http://api.example.com/inventory-management/managed-entities/{id}/install-script-location
//più leggibile
http://api.example.com/inventory-management/managedEntities/{id}/installScriptLocation
//meno leggibile

Quando possibile, preferire lettere minuscole nei percorsi.

http://api.example.org/my-folder/my-doc/1
HTTP://API.EXAMPLE.ORG/my-folder/my-doc/2
http://api.example.org/My-Folder/my-doc/3

Negli esempi, 1 e 2 sono equivalenti; 3 no (per la diversa capitalizzazione di My-Folder).

Le estensioni aumentano la lunghezza e non apportano vantaggi. Il tipo di media si comunica con l’header Content-Type.

http://api.example.com/device-management/managed-devices.xml
/*Do not use it*/
http://api.example.com/device-management/managed-devices
/*This is correct URI*/

Le URI identificano risorse, non azioni; usare i metodi HTTP per indicare l’operazione.

HTTP GET http://api.example.com/device-management/managed-devices
//ottiene l’elenco dei device
HTTP POST http://api.example.com/device-management/managed-devices
//Crea un nuovo device
HTTP GET http://api.example.com/device-management/managed-devices/{id}
//ottiene il device con l’ID specificato
HTTP PUT http://api.example.com/device-management/managed-devices/{id}
//Aggiorna il device con l’ID specificato
HTTP DELETE http://api.example.com/device-management/managed-devices/{id}
//elimina il device con l’ID specificato

Per ordinamento, filtro e paginazione, abilitare parametri in query string invece di definire nuove API:

http://api.example.com/device-management/managed-devices
http://api.example.com/device-management/managed-devices?region=USA
http://api.example.com/device-management/managed-devices?region=USA&brand=XYZ
http://api.example.com/device-management/managed-devices?region=USA&brand=XYZ&sort=installation-date

Http Methods

Le API RESTful consentono di sviluppare applicazioni che coprono tutte le operazioni CRUD. Le linee guida suggeriscono di usare metodi HTTP specifici per ciascun tipo di chiamata al server.

  • HTTP GET

Usare le richieste GET esclusivamente per recuperare la rappresentazione/le informazioni della risorsa — e non per modificarla in alcun modo. Poiché le richieste GET non cambiano lo stato della risorsa, sono considerate metodi sicuri (safe). Inoltre, le API GET dovrebbero essere idempotenti: ripetere la stessa richiesta non deve produrre effetti collaterali e deve restituire lo stesso risultato.

Se la Request-URI si riferisce a un processo che produce dati, devono essere restituiti i dati prodotti.

Per risorse trovate, restituire 200 (OK) con corpo (tipicamente XML o JSON). Se la risorsa non è trovata, 404 (NOT FOUND). Se la richiesta è malformata, 400 (BAD REQUEST).

Esempi:

HTTP GET http://www.appdomain.com/users
HTTP GET http://www.appdomain.com/users?size=20&page=5
HTTP GET http://www.appdomain.com/users/123
HTTP GET http://www.appdomain.com/users/123/address
  • HTTP POST

Usare le API POST per creare nuove risorse subordinate alla risorsa di destinazione. In caso di creazione, restituire 201 (Created) con header Location. Se non viene creata una risorsa individuabile da URI, restituire 200 (OK) o 204 (No Content). Le risposte non sono cacheable salvo presenza di header appropriati. POST non è né sicuro (safe) né idempotente.

Esempi:

HTTP POST http://www.appdomain.com/users
HTTP POST http://www.appdomain.com/users/123/accounts
  • HTTP PUT

Usare le API PUT principalmente per aggiornare risorse esistenti (se la risorsa non esiste, l’API può decidere se crearla). In caso di creazione, 201 (Created); in caso di modifica, 200 (OK) o 204 (No Content). Le risposte non sono cacheable. Differenza con POST: POST su collezioni, PUT su risorsa individuale.

Esempi:

HTTP PUT http://www.appdomain.com/users/123
HTTP PUT http://www.appdomain.com/users/123/accounts/456
  • HTTP DELETE

DELETE rimuove risorse identificate dalla Request-URI. Risposta di successo: 200 (OK) con eventuale corpo, 202 (Accepted) se accodato, 204 (No Content) se senza corpo. DELETE è idempotente (ripetere la chiamata non cambia l’esito; una seconda chiamata può restituire 404 poiché la risorsa è già stata rimossa). Le risposte non sono cacheable.

Esempi:

HTTP DELETE http://www.appdomain.com/users/123
HTTP DELETE http://www.appdomain.com/users/123/accounts/456
  • HTTP PATCH

PATCH effettua aggiornamenti parziali. PUT sostituisce l’intera risorsa; PATCH applica un delta. Esistono sfide pratiche (supporto parziale in browser/server/framework; payload più complesso, ad es. JSON Patch).

Esempio:

HTTP GET /users/1
{"id": 1, "username": "admin", "email": "email@example.org"}
HTTP PATCH /users/1
[
{ "op": "replace", "path": "/email", "value": "new.email@example.org" }
]

Operazioni possibili secondo specifica:

[
{ "op": "test", "path": "/a/b/c", "value": "foo" },
{ "op": "remove", "path": "/a/b/c" },
{ "op": "add", "path": "/a/b/c", "value": [ "foo", "bar" ] },
{ "op": "replace", "path": "/a/b/c", "value": 42 },
{ "op": "move", "from": "/a/b/c", "path": "/a/b/d" },
{ "op": "copy", "from": "/a/b/d", "path": "/a/b/e" }
]

Riepilogo (CRUD vs metodi):

HTTP METHODCRUDINTERA COLLEZIONE (es. /users)ELEMENTO SPECIFICO (es. /users/123)
POSTCreate201 (Created), Location a /users/{id}Evitare POST su risorsa singola
GETRead200 (OK), lista con paginazione/ordinamento/filtro200 (OK) oppure 404 (Not Found)
PUTUpdate/Replace404 (Not Found) salvo update massivo200 (OK) o 204 (No Content); 404 se ID non valido
PATCHPartial Update404 (Not Found) salvo modifica collezione200 (OK) o 204 (No Content); 404 se ID non valido
DELETEDelete404 (Not Found) salvo eliminazione dell’intera collezione (attenzione)200 (OK) o 404 (Not Found)

HATEOAS

HATEOAS (Hypermedia as the Engine of Application State) è un vincolo dell’architettura REST che la differenzia da altre architetture per applicazioni distribuite. L’idea è di includere nella rappresentazione della risorsa collegamenti ipertestuali verso risorse correlate, consentendo al client di navigare dinamicamente lo spazio delle risorse.

Esempio (risposta JSON di una richiesta)

HTTP GET http://api.domain.com/management/departments/10
{
"departmentId": 10,
"departmentName": "Administration",
"locationId": 1700,
"managerId": 200,
"links": [
{
"href": "10/employees",
"rel": "employees",
"type" : "GET"
}
]
}

Il vantaggio è che lo stato applicativo è “guidato” dai link forniti dal server, e non codificato rigidamente nel client.

Richardson Maturity Model

Leonard Richardson ha classificato i servizi web in quattro categorie, in funzione della loro aderenza ai principi REST, considerando tre dimensioni: URI, metodi HTTP e HATEOAS.

Richardson-maturity-model-1 Richardson-maturity-model-2

Descrizione dei livelli:

  • Level Zero: un solo endpoint e un solo metodo (tipicamente POST); nessun uso di URI, metodi o HATEOAS.
  • Level One: uso di molteplici URI per identificare risorse, ma un solo verbo (generalmente POST).
  • Level Two: uso di URI e molti metodi HTTP (CRUD su risorse esposte).
  • Level Three: uso combinato di URI, metodi e HATEOAS; il livello più maturo per rilevabilità (discoverability) e auto-descrittività (self-descriptiveness).

Stateless, auto-definizione e collegamenti tra risorse

Sezione intitolata “Stateless, auto-definizione e collegamenti tra risorse”

Riferimento: https://www.html.it/pag/19598/stateless-autodefinizione-e-collegamenti-tra-risorse/

Le risorse sono concettualmente separate dalle loro rappresentazioni. Il servizio non invia “record” del database, ma rappresentazioni in formati appropriati (scelti tramite negoziazione di contenuto). È opportuno offrire formati standard e, quando utile, multiple rappresentazioni (HTML, XML, JSON) per soddisfare client differenti. Il tipo è indicato via MIME nella risposta (Content-Type). Il client può specificare il formato desiderato con l’header Accept.

Esempio di richiesta:

GET /clienti/1234
HTTP/1.1
Host: www.myapp.com
Accept: application/vnd.myapp.cliente+xml

Le rappresentazioni dovrebbero includere link ipertestuali a risorse correlate (HATEOAS). Esempio XML semplificato:

<ordine>
<numero>12345678</numero>
<data>01/07/2011</data>
<cliente rif="http://www.myapp.com/clienti/1234" />
<articoli>
<articolo rif="http://www.myapp.com/prodotti/98765" />
<articolo rif="http://www.myapp.com/prodotti/43210" />
</articoli>

Le interazioni tra client e server devono essere stateless. Ciò non implica l’assenza di stato applicativo, ma sposta la responsabilità della gestione dello stato sul client (o su meccanismi condivisi espliciti), migliorando la scalabilità e consentendo architetture a cluster senza vincoli di sessione.

Riferimenti:

Un esempio di programma che utilizza le API di GitHub

Sezione intitolata “Un esempio di programma che utilizza le API di GitHub”

Per i Media-Type si veda la documentazione ufficiale.

Questo progetto utilizza la libreria HttpProxyControl.

//file: Repository.cs
using System.Text.Json.Serialization;
namespace GithubAPIClient.Models;
public class Repository
{
[JsonPropertyName("name")]
public string Name { get; set; } = null!;
[JsonPropertyName("description")]
public string Description { get; set; } = null!;
[JsonPropertyName("html_url")]
public Uri GitHubHomeUrl { get; set; } = null!;
[JsonPropertyName("homepage")]
public Uri Homepage { get; set; } = null!;
[JsonPropertyName("watchers")]
public int Watchers { get; set; }
[JsonPropertyName("pushed_at")]
public DateTime LastPushUtc { get; set; }
public DateTime LastPush => LastPushUtc.ToLocalTime();
}
//file: Program.cs
using System.Net.Http.Headers;
using System.Net.Http.Json;
using System.Text.Json;
using GithubAPIClient.Models;
internal class Program
{
static async Task Main(string[] args)
{
using HttpClient client = new();
client.DefaultRequestHeaders.Accept.Clear();
client.DefaultRequestHeaders.Accept.Add(
new MediaTypeWithQualityHeaderValue("application/vnd.github.v3+json"));
client.DefaultRequestHeaders.Add("User-Agent", ".NET Foundation Repository Reporter");
var repositories = await ProcessRepositoriesAsync2(client);
foreach (var repo in repositories)
{
Console.WriteLine($"Name: {repo.Name}");
Console.WriteLine($"Homepage: {repo.Homepage}");
Console.WriteLine($"GitHub: {repo.GitHubHomeUrl}");
Console.WriteLine($"Description: {repo.Description}");
Console.WriteLine($"Watchers: {repo.Watchers:#,0}");
Console.WriteLine($"{repo.LastPush}");
Console.WriteLine();
}
// VERSIONE 1: GetStreamAsync + JsonSerializer.DeserializeAsync
// PRO: Efficiente con la memoria - deserializza in streaming
// Controllo manuale sullo stream se necessario
// CONTRO: Più verbosa rispetto a GetFromJsonAsync
// QUANDO USARLA: Quando serve controllo diretto sullo stream (es. cancellazione, progress)
static async Task<List<Repository>> ProcessRepositoriesAsync(HttpClient client)
{
await using Stream stream =
await client.GetStreamAsync("https://api.github.com/orgs/dotnet/repos");
var repositories =
await JsonSerializer.DeserializeAsync<List<Repository>>(stream);
return repositories ?? [];
}
// VERSIONE 2: GetFromJsonAsync ⭐ CONSIGLIATA
// PRO: Concisa e leggibile - combina download e deserializzazione
// Efficiente con la memoria - deserializza in streaming
// Gestione automatica degli errori HTTP
// Usa la configurazione JSON centralizzata dell'HttpClient
// CONTRO: Meno controllo sulla risposta HTTP
// QUANDO USARLA: Per la maggior parte dei casi - è la scelta preferita
static async Task<List<Repository>> ProcessRepositoriesAsync2(HttpClient client)
{
var repositories =
await client.GetFromJsonAsync<List<Repository>>("https://api.github.com/orgs/dotnet/repos");
return repositories ?? [];
}
// VERSIONE 3: GetStringAsync + JsonSerializer.Deserialize ❌ SCONSIGLIATA
// PRO: Semplice da capire - due passaggi distinti
// CONTRO: Inefficiente - carica tutto il JSON in memoria come stringa
// Doppio consumo di memoria (stringa + oggetto deserializzato)
// Non streaming - problematico per file JSON di grandi dimensioni
// QUANDO USARLA: Solo per JSON molto piccoli o per debugging/logging della risposta
static async Task<List<Repository>> ProcessRepositoriesAsync3(HttpClient client)
{
string jsonString =
await client.GetStringAsync("https://api.github.com/orgs/dotnet/repos");
var repositories =
JsonSerializer.Deserialize<List<Repository>>(jsonString);
return repositories ?? [];
}
// VERSIONE 4: GetAsync + ReadFromJsonAsync
// PRO: Massimo controllo - accesso completo a HttpResponseMessage
// Possibilità di ispezionare header, status code, ecc.
// Gestione personalizzata degli errori
// Efficiente - deserializzazione in streaming
// CONTRO: Più verbosa
// QUANDO USARLA: Quando serve accedere agli header di risposta o gestire
// gli status code in modo personalizzato
static async Task<List<Repository>> ProcessRepositoriesAsync4(HttpClient client)
{
HttpResponseMessage response =
await client.GetAsync("https://api.github.com/orgs/dotnet/repos");
response.EnsureSuccessStatusCode();
var repositories =
await response.Content.ReadFromJsonAsync<List<Repository>>();
return repositories ?? [];
}
}
}

Principali classi di riferimento per la gestione delle richieste HTTP

Sezione intitolata “Principali classi di riferimento per la gestione delle richieste HTTP”

Per un approfondimento accademico e tecnico, si consiglia la consultazione delle seguenti risorse: