Block Image

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:

  1. Versioning by URL
  2. 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:

  1. Nel campo Accept dell'header quale versione del payload di risposta desidera utilizzare.
  2. 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.