REST in 2026: Best Practice, Versioning and the Richardson Maturity Model
Most APIs that call themselves "RESTful" are not. They use HTTP like transport, JSON as format, and stop there. The Richardson Maturity Model, introduced by Leonard Richardson in 2008 and popularized by Martin Fowler, provides a scale in four levels that distinguishes a generic HTTP API (level 0) from a truly API RESTful (level 3 with HATEOAS).
But architectural perfectionism is not the purpose of this guide. The goal is to teach you to build REST APIs that are correct, maintainable and well documented: use appropriate HTTP semantics, implement sustainable versioning over time, manage cache effectively with ETag, and document the contract with OpenAPI 3.1. These are the real differentiators between a professional API and one that causes problems for consumers.
What You Will Learn
- The 4 levels of the Richardson Maturity Model with concrete examples
- Correct HTTP semantics: methods, status codes, headers
- Idempotence and safe methods: the theoretical bases with practical applications
- Versioning strategies: URI, header, and versioning via additive changes
- ETag and conditional requests for efficient caching
- OpenAPI 3.1: document structure and best practices
Richardson Maturity Model: The 4 Levels
The Richardson Maturity Model measures the "RESTfulness" of an API on a scale 0 to 3. Understanding these levels helps identify architectural gaps into existing APIs and build new ones at the correct level.
Level 0: HTTP as Transport (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
Level 1: Resources (Meaningful URI)
// 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
Level 2: HTTP Verbs (the standard level)
// 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
Level 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: Is it worth it?
HATEOAS is theoretically elegant but rarely implemented in practice. The reasons:
- Significantly increases response size
- However, clients often "hardcode" URLs for performance
- Requires specific tooling to navigate links
- Most teams use OpenAPI as a contract instead
Level 2 + OpenAPI 3.1 and the pragmatic sweet spot for most APIs in 2026. HATEOAS makes sense mainly in public APIs where URL stability is long term term and criticism.
Correct HTTP semantics
Using HTTP verbs with the correct semantics is not just an aesthetic issue: it has impact caching, idempotency, and the ability of clients to retract requests securely.
// 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 it means that the request has no server-side side effects (the server does not change state). A client can "press F5" on a GET without consequences.
Idempotent means that multiple identical requests produce the same result of a single request. Crucial for retries: if the network goes down during a DELETE, the client can try again without fear of deleting more than once.
Status Codes: Use the Right Ones
The abuse of 200 OK for everything (including body errors) and one of the anti-patterns
more common. Using the correct codes allows clients, proxies, and monitoring tools to
interpret the answers correctly:
// 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: The Important Distinction
The confusion between PUT and PATCH is common but has concrete implications:
// 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" }
]
Versioning strategies
Versioning is one of the most important decisions in designing a public API. Once that clients depend on an API, changing the contract breaks their applications. Le Main strategies have different trade-offs:
1. URI Versioning (most common)
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 via Additive Changes (best)
// 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 and Conditional Requests
L'ETag (Entity Tag) and an HTTP mechanism to manage cache and optimistic competition. Each resource has a hash or timestamp that identifies its version current:
// 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
Documenting with OpenAPI 3.1
OpenAPI 3.1 is the industry standard for documenting REST APIs. A nice OpenAPI document written serves as a formal contract, enables client SDK generation, and powers Interactive documentation with Swagger UI or 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 for Error Responses
A consistent structure for error responses dramatically improves developer API consumer experience. The standard RFC 9457 (Problem Details) and became the preferred choice in 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
}
Conclusions and Next Steps
A professional REST API in 2026 operates at level 2 of the Richardson Maturity Model (resources + correct HTTP verbs + appropriate status codes), use ETag for caching and optimistic concurrency, adopts sustainable versioning based on additive changes, and documents the formal contract with OpenAPI 3.1. It is not necessary to reach HATEOAS level 3 in most contexts.
The next article examines GraphQL in depth: how the system works of resolvers, because the N+1 problem is the most common architectural risk, and as DataLoader resolves batching of database queries.
Series: API Design — REST, GraphQL, gRPC, and tRPC Compared
- Article 1: The API Landscape in 2026 — Decision Matrix
- Article 2 (this): REST in 2026 — Best Practice, Versioning and the Richardson Maturity Model
- Article 3: GraphQL — Query Language, Resolver and the N+1 Problem
- Article 4: GraphQL Federation — Supergraph, Subgraph and Apollo Router
- Article 5: gRPC — Protobuf, Performance and Service-to-Service Communication
- Article 6: tRPC — Type Safety End-to-End without Code Generation
- Article 7: Webhooks — Patterns, Security and Retry Logic
- Article 8: API Versioning — URI, Header and Deprecation Policy
- Article 9: Rate Limiting and Throttling — Algorithms and Implementations
- Article 10: Hybrid API Architecture — REST + tRPC + gRPC in 2026







