REST nel 2026: Best Practice, Versioning e Richardson Maturity Model
La maggior parte delle API che si autodefiniscono "RESTful" non lo sono. Usano HTTP come trasporto, JSON come formato, e si fermano li. Il Richardson Maturity Model, introdotto da Leonard Richardson nel 2008 e popolarizzato da Martin Fowler, fornisce una scala in quattro livelli che distingue un'API HTTP generica (livello 0) da una API veramente RESTful (livello 3 con HATEOAS).
Ma il perfezionismo architetturale non e lo scopo di questa guida. L'obiettivo e insegnarti a costruire REST API che siano corrette, mantenibili e ben documentate: usa la semantica HTTP appropriata, implementa un versioning sostenibile nel tempo, gestisci la cache in modo efficace con ETag, e documenta il contratto con OpenAPI 3.1. Questi sono i differenziatori reali tra un'API professionale e una che causa problemi ai consumatori.
Cosa Imparerai
- I 4 livelli del Richardson Maturity Model con esempi concreti
- Semantica HTTP corretta: metodi, status codes, headers
- Idempotenza e safe methods: le basi teoriche con applicazioni pratiche
- Strategie di versioning: URI, header, e versioning tramite additive changes
- ETag e conditional requests per caching efficiente
- OpenAPI 3.1: struttura del documento e best practice
Richardson Maturity Model: I 4 Livelli
Il Richardson Maturity Model misura il "grado di RESTfulness" di un'API su una scala da 0 a 3. Comprendere questi livelli aiuta a identificare le lacune architetturali nelle API esistenti e a costruirne di nuove al livello corretto.
Livello 0: HTTP come Trasporto (il Tunneling)
// Livello 0: tutto su un unico endpoint, azione nel body
POST /api
Content-Type: application/json
{
"action": "getUser",
"id": 123
}
POST /api
{
"action": "createUser",
"name": "Federico",
"email": "federico@example.com"
}
// HTTP e solo un canale: la semantica e tutta nell'applicazione
// Nessun caching possibile, nessuna semantica uniforme
Livello 1: Risorse (URI meaningful)
// Livello 1: URL per risorse, ma ancora solo POST
POST /users/123 // Non ha senso: POST per leggere?
POST /users/create
POST /users/delete/123
POST /users/get/all
// Migliore, ma i verbi HTTP non sono usati semanticamente
Livello 2: Verbi HTTP (il livello standard)
// Livello 2: URL meaningful + verbi HTTP corretti + status codes
GET /users -> 200 OK con lista
GET /users/123 -> 200 OK con utente, 404 Not Found
POST /users -> 201 Created con Location header
PUT /users/123 -> 200 OK aggiornato, 404 Not Found
PATCH /users/123 -> 200 OK parzialmente aggiornato
DELETE /users/123 -> 204 No Content, 404 Not Found
// Questo e il livello che la maggior parte delle API raggiunge
// ed e generalmente sufficiente per la produzione
Livello 3: HATEOAS (Hypermedia as the Engine of Application State)
// Livello 3: ogni risposta contiene link alle azioni disponibili
GET /users/123
-> 200 OK
{
"id": 123,
"name": "Federico",
"email": "federico@example.com",
"status": "active",
"_links": {
"self": { "href": "/users/123" },
"orders": { "href": "/users/123/orders" },
"deactivate": { "href": "/users/123/deactivate", "method": "POST" },
"update": { "href": "/users/123", "method": "PUT" }
}
}
// Il client non deve "sapere" a priori gli URL: li scopre dalle risposte
// Permette di cambiare URL senza rompere i client (in teoria)
HATEOAS: Vale la Pena?
HATEOAS e teoricamente elegante ma raramente implementato nella pratica. Le ragioni:
- Aumenta significativamente la dimensione delle risposte
- I client comunque spesso "hardcodano" gli URL per performance
- Richiede tooling specifico per navigare i link
- La maggior parte dei team usa OpenAPI come contratto invece
Livello 2 + OpenAPI 3.1 e il pragmatic sweet spot per la maggior parte delle API nel 2026. HATEOAS ha senso principalmente in API pubbliche dove la stabilita degli URL nel lungo termine e critica.
Semantica HTTP Corretta
Usare i verbi HTTP con la semantica corretta non e solo una questione estetica: impatta caching, idempotenza, e la capacita dei client di ritrare le richieste in modo sicuro.
// Proprietà dei metodi HTTP
Metodo | Safe | Idempotente | Body richiesta | Uso corretto
--------|------|-------------|----------------|----------------------------------
GET | SI | SI | No | Lettura, query, ricerca
HEAD | SI | SI | No | Verifica esistenza, metadata
OPTIONS | SI | SI | No | CORS preflight, capabilities
POST | NO | NO | SI | Creazione, azioni non-idempotenti
PUT | NO | SI | SI | Sostituzione completa di risorsa
PATCH | NO | NO* | SI | Modifica parziale
DELETE | NO | SI | Opzionale | Eliminazione
// *PATCH puo essere reso idempotente con patch semantics JSON Patch (RFC 6902)
Safe significa che la richiesta non ha effetti collaterali lato server (il server non cambia stato). Un client puo "premere F5" su un GET senza conseguenze.
Idempotente significa che richieste multiple identiche producono lo stesso risultato di una singola richiesta. Cruciale per i retry: se la rete cade durante un DELETE, il client puo riprovare senza paura di eliminare piu di una volta.
Status Codes: Usa Quelli Giusti
L'abuso di 200 OK per tutto (inclusi gli errori nel body) e uno degli anti-pattern
piu comuni. Usare i codici corretti permette ai client, proxy, e tool di monitoring di
interpretare correttamente le risposte:
// Status codes piu importanti con esempi di uso corretto
// 2xx: Success
200 OK - GET, PUT, PATCH con risorsa nel body
201 Created - POST che crea una risorsa (+ Location header)
202 Accepted - Operazione asincrona accettata (non ancora completata)
204 No Content - DELETE, PUT/PATCH senza body di risposta
// 3xx: Redirection
301 Moved Permanently - Redirect permanente (aggiornare i bookmark)
302 Found - Redirect temporaneo
304 Not Modified - ETag/If-None-Match: risorsa non cambiata, usa la cache
// 4xx: Client Error (il client ha sbagliato)
400 Bad Request - Input malformato, schema validation fallita
401 Unauthorized - Non autenticato (serve login)
403 Forbidden - Autenticato ma non autorizzato
404 Not Found - Risorsa non trovata
405 Method Not Allowed - Metodo HTTP non supportato su questo endpoint
409 Conflict - Conflitto di stato (es: email gia esistente)
410 Gone - Risorsa eliminata permanentemente (vs 404)
422 Unprocessable Entity - Sintassi ok ma semantica invalida
429 Too Many Requests - Rate limit raggiunto (+ Retry-After header)
// 5xx: Server Error (colpa del server)
500 Internal Server Error - Errore generico non gestito
502 Bad Gateway - Errore dal backend upstream
503 Service Unavailable - Server temporaneamente non disponibile
504 Gateway Timeout - Timeout dal backend upstream
PUT vs PATCH: La Distinzione Importante
La confusione tra PUT e PATCH e comune ma ha implicazioni concrete:
// PUT: sostituzione COMPLETA della risorsa (idempotente)
// Se ometti un campo, viene azzerato!
PUT /users/123
{
"name": "Federico Calo",
"email": "federico@example.com"
// Se il campo "phone" non e incluso, viene rimosso!
}
// PATCH: modifica PARZIALE (solo i campi specificati)
PATCH /users/123
{
"name": "Federico Calo"
// Solo name viene aggiornato, email e phone rimangono invariati
}
// PATCH con JSON Patch (RFC 6902): piu preciso e idempotente
PATCH /users/123
Content-Type: application/json-patch+json
[
{ "op": "replace", "path": "/name", "value": "Federico Calo" },
{ "op": "add", "path": "/phone", "value": "+39 333 1234567" },
{ "op": "remove", "path": "/tempNote" }
]
Strategie di Versioning
Il versioning e una delle decisioni piu importanti nel design di un'API pubblica. Una volta che i client dipendono da un'API, cambiare il contratto rompe le loro applicazioni. Le strategie principali hanno trade-off diversi:
1. URI Versioning (il piu comune)
GET /api/v1/users // Versione 1
GET /api/v2/users // Versione 2 con campi aggiuntivi
// Vantaggi:
// - Visibile e ovvio
// - Cacheable a livello HTTP (l'URL e diverso)
// - Facile da esplorare con il browser
// - Semplice da loggare e monitorare
// Svantaggi:
// - "Sporco" semanticamente (la versione non e parte della risorsa)
// - Proliferazione di URL nel tempo
2. Header Versioning
GET /api/users
Accept: application/vnd.myapi.v2+json
// oppure
API-Version: 2
// Vantaggi:
// - URL "puliti"
// - Piu vicino alla semantica HTTP originale
// Svantaggi:
// - Non cacheable con HTTP standard (Cache-Vary header necessario)
// - Invisibile dalla URL (difficile da debuggare)
// - Meno intuitivo per i nuovi consumatori dell'API
3. Versioning tramite Additive Changes (il migliore)
// Strategia: non cambiare mai, solo aggiungere (non rompere mai i client)
// V1 risposta:
GET /api/users/123
{
"id": 123,
"name": "Federico",
"email": "federico@example.com"
}
// Aggiungi campi senza versione (i client vecchi ignorano i nuovi campi):
GET /api/users/123
{
"id": 123,
"name": "Federico",
"email": "federico@example.com",
"createdAt": "2025-01-15T10:30:00Z", // AGGIUNTO: non rompe i client vecchi
"avatarUrl": null // AGGIUNTO: nullable per retrocompat
}
// Quando DEVI rompere (rare):
// - Rimuovere un campo -> versione nuova
// - Cambiare tipo di un campo -> versione nuova
// - Cambiare semantica di un campo -> versione nuova
ETag e Conditional Requests
L'ETag (Entity Tag) e un meccanismo HTTP per gestire la cache e la concorrenza ottimistica. Ogni risorsa ha un hash o timestamp che identifica la sua versione corrente:
// Server: risposta con ETag
GET /users/123
->
200 OK
ETag: "abc123def456"
Cache-Control: max-age=300
{
"id": 123,
"name": "Federico",
"version": 3
}
// Client: richiesta condizionale con If-None-Match
GET /users/123
If-None-Match: "abc123def456"
->
304 Not Modified // Risorsa non cambiata, usa la cache!
// Nessun body = traffico ridotto
// Se la risorsa e cambiata:
GET /users/123
If-None-Match: "abc123def456"
->
200 OK
ETag: "xyz789new123" // Nuovo ETag
{ /* dati aggiornati */ }
// ETag per concorrenza ottimistica (prevenire aggiornamenti in conflitto):
PUT /users/123
If-Match: "abc123def456" // "Aggiorna SOLO se la versione e ancora questa"
{...}
->
200 OK // Aggiornamento riuscito, nessun conflitto
// oppure
412 Precondition Failed // Qualcun altro ha modificato la risorsa!
// Il client deve rileggere prima di riaggiornare
Documentare con OpenAPI 3.1
OpenAPI 3.1 e lo standard industriale per documentare REST API. Un documento OpenAPI ben scritto serve come contratto formale, abilita la generazione di SDK client, e alimenta documentazione interattiva con Swagger UI o Redoc:
// openapi.yaml - Struttura base di un documento OpenAPI 3.1
openapi: 3.1.0
info:
title: User Management API
version: 1.2.0
description: |
API per la gestione degli utenti dell'applicazione.
## Autenticazione
Usa Bearer token JWT nell'header Authorization.
contact:
name: API Support
email: api@example.com
license:
name: MIT
servers:
- url: https://api.example.com/v1
description: Production
- url: https://staging-api.example.com/v1
description: Staging
paths:
/users:
get:
operationId: listUsers
summary: Lista utenti
tags: [Users]
parameters:
- name: page
in: query
schema: { type: integer, minimum: 1, default: 1 }
- name: limit
in: query
schema: { type: integer, minimum: 1, maximum: 100, default: 20 }
- name: search
in: query
schema: { type: string }
responses:
'200':
description: Lista utenti paginata
content:
application/json:
schema:
$ref: '#/components/schemas/UserListResponse'
'401':
$ref: '#/components/responses/Unauthorized'
post:
operationId: createUser
summary: Crea utente
tags: [Users]
requestBody:
required: true
content:
application/json:
schema:
$ref: '#/components/schemas/CreateUserRequest'
responses:
'201':
description: Utente creato
headers:
Location:
schema: { type: string }
description: URL del nuovo utente
content:
application/json:
schema:
$ref: '#/components/schemas/User'
'400':
$ref: '#/components/responses/BadRequest'
'409':
description: Email gia esistente
components:
schemas:
User:
type: object
required: [id, name, email, createdAt]
properties:
id: { type: integer, format: int64, readOnly: true }
name: { type: string, minLength: 1, maxLength: 100 }
email: { type: string, format: email }
createdAt: { type: string, format: date-time, readOnly: true }
CreateUserRequest:
type: object
required: [name, email, password]
properties:
name: { type: string, minLength: 1, maxLength: 100 }
email: { type: string, format: email }
password: { type: string, minLength: 8, writeOnly: true }
securitySchemes:
bearerAuth:
type: http
scheme: bearer
bearerFormat: JWT
security:
- bearerAuth: []
Pattern per Error Responses
Una struttura consistente per le risposte di errore migliora drasticamente la developer experience dei consumatori dell'API. Lo standard RFC 9457 (Problem Details) e diventato la scelta preferita nel 2026:
// RFC 9457 Problem Details for HTTP APIs
// Content-Type: application/problem+json
// 400 Bad Request
{
"type": "https://example.com/errors/validation-error",
"title": "Validation Error",
"status": 400,
"detail": "The request body contains invalid data",
"instance": "/api/users",
"errors": [
{
"field": "email",
"message": "Invalid email format",
"value": "not-an-email"
},
{
"field": "password",
"message": "Password must be at least 8 characters",
"value": null
}
]
}
// 409 Conflict
{
"type": "https://example.com/errors/duplicate-email",
"title": "Duplicate Email",
"status": 409,
"detail": "An account with this email already exists",
"instance": "/api/users",
"email": "federico@example.com"
}
// 429 Too Many Requests
{
"type": "https://example.com/errors/rate-limited",
"title": "Rate Limit Exceeded",
"status": 429,
"detail": "Too many requests. Retry after 60 seconds.",
"retryAfter": 60
}
Conclusioni e Prossimi Passi
Una REST API professionale nel 2026 opera al livello 2 del Richardson Maturity Model (risorse + verbi HTTP corretti + status codes appropriati), usa ETag per caching e concorrenza ottimistica, adotta un versioning sostenibile basato su additive changes, e documenta il contratto formale con OpenAPI 3.1. Non e necessario raggiungere il livello 3 HATEOAS nella maggior parte dei contesti.
Il prossimo articolo esamina GraphQL in profondita: come funziona il sistema di resolver, perche il problema N+1 e il rischio architetturale piu comune, e come DataLoader risolve il batching delle query al database.
Serie: API Design — REST, GraphQL, gRPC e tRPC a Confronto
- Articolo 1: Il Panorama delle API nel 2026 — Matrice Decisionale
- Articolo 2 (questo): REST nel 2026 — Best Practice, Versioning e Richardson Maturity Model
- Articolo 3: GraphQL — Query Language, Resolver e il Problema N+1
- Articolo 4: GraphQL Federation — Supergraph, Subgraph e Apollo Router
- Articolo 5: gRPC — Protobuf, Performance e Comunicazione Service-to-Service
- Articolo 6: tRPC — Type Safety End-to-End senza Code Generation
- Articolo 7: Webhooks — Pattern, Sicurezza e Retry Logic
- Articolo 8: API Versioning — URI, Header e Deprecation Policy
- Articolo 9: Rate Limiting e Throttling — Algoritmi e Implementazioni
- Articolo 10: Architettura API Ibrida — REST + tRPC + gRPC nel 2026







