Introduzione
Quando si vogliono progettare API REST scalabili, manutenibili e compatibili nel tempo, una delle decisioni più importanti riguarda il versioning. In questo articolo ti guiderò tra due strategie molto diffuse: il versioning tramite URL e il versioning tramite content negotiation, spiegando vantaggi e svantaggi, con esempi reali in Spring Boot 3 e OpenAPI. Scoprirai qual è la soluzione più adatta al tuo caso d'uso e come implementarla correttamente.
Se vuoi approfondire la progettazioni di API, con architettura ed esempi pratici, leggi il mio libro Spring Boot 3 API Mastery disponibile su Amazon.
Nel primo capitolo del mio libro Spring Boot 3 API Mastery esploro le basi della progettazione di API REST, ponendo fin da subito l'accento su un tema fondamentale per ogni progetto a lungo termine: il versioning.
Versionare correttamente le API è essenziale per garantire compatibilità, evoluzione e stabilità ai client che le consumano. In questo articolo approfondiamo due strategie comuni, entrambe supportate da Spring Boot 3 e ben documentate nella specifica OpenAPI:
- Versioning by URL
- Versioning by Content Negotiation
Utilizzeremo come esempio una API POST che inserisce una risorsa libro. Il payload di richiesta e di risposta sarà uguale.
La prima versione dell'API ha il seguente payload:
components:
schemas:
BookV1:
required:
- isbn
- title
- author
type: object
properties:
isbn:
type: string
title:
type: string
author:
type: string
mentre la seconda versione ha un payload che dettaglia meglio le informazioni dell'autore:
components:
schemas:
BookV2:
required:
- isbn
- title
- author
type: object
properties:
isbn:
type: string
title:
type: string
author:
$ref: "#/components/schemas/AuthorResponse"
AuthorResponse:
properties:
firstname:
type: string
lastname:
type: string
Utilizzando questo esempio, possiamo vedere in azione la versione tramite content negotiation sia per il body di richiesta sia per quello di risposta.
1. Versioning by URL: chiaro, esplicito, efficace
Il versioning tramite URL è forse la strategia più conosciuta e diffusa. Consiste nell'includere il numero di versione direttamente nel path dell'endpoint, ad esempio:
curl --json \
'{"isbn": "978-9365898088",
"title": "Spring Boot 3 API Mastery",
"author": "Vincenzo Racca"}' \
localhost:8080/api/books/v1 | jq
{
"isbn": "978-9365898088",
"title": "Spring Boot 3 API Mastery",
"author": "Vincenzo Racca"
}
curl --json \
'{"isbn": "978-9365898088",
"title": "Spring Boot 3 API Mastery",
"author": { "firstname": "Vincenzo", "lastname": "Racca"}}' \
localhost:8080/api/books/v2 | jq
{
"isbn": "978-9365898088",
"title": "Spring Boot 3 API Mastery",
"author": {
"firstname": "Vincenzo",
"lastname": "Racca"
}
}
Esempio OpenAPI
/api/books/v1:
post:
tags:
- book
summary: Insert a new book
operationId: insertBookV1
requestBody:
content:
application/json:
schema:
$ref: './schemas-v1.yml#/components/schemas/BookV1'
responses:
'200':
description: Book inserted
content:
application/json:
schema:
$ref: './schemas-v1.yml#/components/schemas/BookV1'
/api/books/v2:
post:
tags:
- book
summary: Insert a new book
operationId: insertBookV2
requestBody:
content:
application/json:
schema:
$ref: './schemas-v2.yml#/components/schemas/BookV2'
responses:
'200':
description: Book inserted
content:
application/json:
schema:
$ref: './schemas-v2.yml#/components/schemas/BookV2'
In Spring Boot
Con Spring Boot 3 basta creare un metodo nel controller per ogni versione del path dell'API:
@RestController
public class BookController {
@PostMapping("/api/books/v1")
public ResponseEntity<BookV1> insertBookV1(@RequestBody BookV1 request) {
return ResponseEntity.ok(request);
}
@PostMapping("/api/books/v2")
public ResponseEntity<BookV2> insertBookV2(@RequestBody BookV2 request) {
return ResponseEntity.ok(request);
}
}
Pro
- Visibilità immediata della versione richiesta.
- Facilmente gestibile nei gateway, nei log e nella documentazione.
- Supportato nativamente da plugin come OpenAPI Generator.
Contro
- Meno "RESTful" secondo i puristi: l'URL rappresenta una risorsa, non una versione.
- Con molte versioni, la struttura del codice può diventare più verbosa e ridondante.
2. Versioning by Content Negotiation: elegante, ma più complesso
Questa strategia si basa sul concetto di content negotiation dell'HTTP. Il client specifica:
- Nel campo
Accept
dell'header quale versione del payload di risposta desidera utilizzare. - Nel campo
Content-Type
dell'header quale versione del payload di richiesta desidera utilizzare.
curl --json \
'{"isbn": "978-9365898088",
"title": "Spring Boot 3 API Mastery",
"author": "Vincenzo Racca"}' \
-H "Content-Type: application/vnd.vincenzoracca.v1+json" \
-H "Accept: application/vnd.vincenzoracca.v1+json" \
localhost:8080/api/books | jq
{
"isbn": "978-9365898088",
"title": "Spring Boot 3 API Mastery",
"author": "Vincenzo Racca"
}
curl --json \
'{"isbn": "978-9365898088",
"title": "Spring Boot 3 API Mastery",
"author": { "firstname": "Vincenzo", "lastname": "Racca"}}' \
-H "Content-Type: application/vnd.vincenzoracca.v2+json" \
-H "Accept: application/vnd.vincenzoracca.v2+json" \
localhost:8080/api/books | jq
Esempio OpenAPI
paths:
/api/books:
post:
tags:
- book
summary: Insert a new book
operationId: insertBook
requestBody:
content:
application/vnd.vincenzoracca.v1+json:
schema:
$ref: './schemas-v1.yml#/components/schemas/BookV1'
application/vnd.vincenzoracca.v2+json:
schema:
$ref: './schemas-v2.yml#/components/schemas/BookV2'
responses:
'200':
description: Book inserted
content:
application/vnd.vincenzoracca.v1+json:
schema:
$ref: './schemas-v1.yml#/components/schemas/BookV1'
application/vnd.vincenzoracca.v2+json:
schema:
$ref: './schemas-v2.yml#/components/schemas/BookV2'
In Spring Boot
Spring Boot consente di creare diversi metodi nel controller per mappare lo stesso path purché il media-type sia differente.
@RestController
public class BookController {
@PostMapping(
value = "/api/books",
produces = {"application/vnd.vincenzoracca.v1+json"},
consumes = "application/vnd.vincenzoracca.v1+json")
public ResponseEntity<BookV1> insertBookV1cn(@RequestBody BookV1 request) {
return ResponseEntity.ok(request);
}
@PostMapping(
value = "/api/books",
produces = "application/vnd.vincenzoracca.v2+json",
consumes = "application/vnd.vincenzoracca.v2+json")
public ResponseEntity<BookV2> insertBookV2cn(@RequestBody BookV2 request) {
return ResponseEntity.ok(request);
}
}
Pro
- Mantiene gli endpoint "puliti" e coerenti con la semantica REST.
- Più flessibile nella gestione delle versioni.
Contro
- Può richiedere maggiore configurazione per i framework che non lo supportano nativamente (ma se utilizzi Spring Boot, non hai questo problema).
- Meno trasparente: la versione non è visibile nell'URL.
- Non sempre supportata da gateway/API proxy.
- Non supportata da OpenAPI Generator (ISSUE##11000).
Quale strategia scegliere?
Come suggerito nel libro, la scelta dipende molto dal contesto:
- Se stai costruendo API pubbliche, con molte integrazioni esterne, versioning by URL è spesso preferibile per la sua chiarezza e semplicità d'integrazione.
- Se punti a un'architettura più elegante e stai costruendo API private, content negotiation può offrire maggiore flessibilità.
In ogni caso, documentare correttamente le versioni è cruciale: qui entra in gioco OpenAPI, che permette di rappresentare in modo formale le diverse versioni e i loro media type.
Conclusione
Il versioning non è solo una scelta tecnica, ma una vera e propria strategia di evoluzione delle tue API. Qualunque approccio tu scelga, l'importante è essere coerente e trasparente nei confronti degli sviluppatori che consumeranno i tuoi servizi.
Se vuoi approfondire il tema della progettazione delle API, dai un'occhiata al primo capitolo del mio libro Spring Boot 3 API Mastery.
Consiglio inoltre una lettura anche delle linee guida dettate da Zalando: Zalando RESTful API and Event Guidelines.