API REST in Laravel: API Resource e trasformazione dei dati

API REST in Laravel: API Resource e trasformazione dei dati

Nella puntata precedente restituivamo i modelli Eloquent direttamente, esponendo la struttura grezza della tabella. In questa puntata introduciamo uno strato di trasformazione: le API Resource. Vedremo come dare alle risposte una forma pulita e stabile, come includere campi in modo condizionale, come rappresentare le relazioni e come gestire la paginazione delle collezioni. Al termine il nostro catalogo restituirà JSON progettato per il client, non un semplice riflesso del database.

Perché non restituire il modello grezzo

Serializzare direttamente un modello ha diversi difetti. Espone il nome esatto delle colonne, legando il contratto pubblico dell'API ai dettagli interni dello schema: se un domani rinominassimo una colonna, romperemmo tutti i client. Rende difficile nascondere campi sensibili o mostrarne di calcolati. Non offre un punto centralizzato dove decidere il formato di date e numeri. Le API Resource risolvono tutto questo interponendo una classe fra il modello e il JSON: il modello resta libero di evolvere, mentre la risorsa definisce con precisione il contratto verso l'esterno.

Creare la prima risorsa

Generiamo una risorsa per il libro con Artisan.

php artisan make:resource BookResource

Il comando crea app/Http/Resources/BookResource.php. Il cuore della classe è il metodo toArray, che riceve la richiesta e restituisce l'array che verrà serializzato in JSON. Qui decidiamo esattamente quali campi mostrare e come nominarli.

<?php

namespace App\Http\Resources;

use Illuminate\Http\Request;
use Illuminate\Http\Resources\Json\JsonResource;

class BookResource extends JsonResource
{
    // Definisce la rappresentazione pubblica di un libro
    public function toArray(Request $request): array
    {
        return [
            'id'         => $this->id,
            'title'      => $this->title,
            'author'     => $this->author,
            'year'       => $this->year,
            'isbn'       => $this->isbn,
            // Esponiamo la data in formato ISO 8601, non nel formato grezzo del DB
            'created_at' => $this->created_at->toIso8601String(),
        ];
    }
}

All'interno della risorsa, $this delega automaticamente al modello sottostante: $this->title legge l'attributo title del libro. Notiamo che abbiamo scelto di non esporre updated_at e di formattare created_at in modo esplicito. La risposta è ora una decisione deliberata, non un effetto collaterale della struttura della tabella.

Usare la risorsa nel controller

Modifichiamo BookController per far passare i modelli attraverso la risorsa. Per un singolo libro istanziamo la risorsa; per una collezione usiamo il metodo statico collection.

<?php

namespace App\Http\Controllers;

use App\Http\Resources\BookResource;
use App\Models\Book;
use Illuminate\Http\Request;

class BookController extends Controller
{
    public function index()
    {
        $books = Book::query()->latest()->get();

        // Trasforma l'intera collezione tramite la risorsa
        return BookResource::collection($books);
    }

    public function store(Request $request)
    {
        $book = Book::create($request->only(['title', 'author', 'year', 'isbn']));

        // Avvolge la risorsa in una risposta con codice 201
        return (new BookResource($book))
            ->response()
            ->setStatusCode(201);
    }

    public function show(Book $book)
    {
        return new BookResource($book);
    }

    public function update(Request $request, Book $book)
    {
        $book->update($request->only(['title', 'author', 'year', 'isbn']));

        return new BookResource($book);
    }
}

Quando restituiamo una risorsa o una collezione di risorse, Laravel si occupa di serializzarle in JSON e di impostare l'intestazione corretta. Per il metodo store, dovendo forzare il codice 201, convertiamo esplicitamente la risorsa in una risposta con response() e ne impostiamo lo stato.

Il wrapper data

Se interroghiamo ora l'endpoint di dettaglio, noteremo che il libro è avvolto in una chiave data.

{
  "data": {
    "id": 1,
    "title": "Il nome della rosa",
    "author": "Umberto Eco",
    "year": 1980,
    "isbn": "978-88-452-0705-0",
    "created_at": "2026-03-17T09:12:44+00:00"
  }
}

Questo avvolgimento è intenzionale e utile: crea uno spazio dove collocare, accanto ai dati, informazioni accessorie come i metadati di paginazione. È una convenzione diffusa nelle API REST e conviene mantenerla per coerenza. Se per qualche ragione volessimo rimuoverlo del tutto, potremmo chiamare JsonResource::withoutWrapping() in un service provider, ma sconsigliamo di farlo senza un motivo preciso.

Includere campi in modo condizionale

Talvolta un campo va mostrato solo in certe condizioni. Le API Resource offrono l'helper when, che aggiunge una chiave soltanto se una condizione è vera. Supponiamo di voler esporre un flag interno soltanto agli utenti autenticati.

public function toArray(Request $request): array
{
    return [
        'id'     => $this->id,
        'title'  => $this->title,
        'author' => $this->author,
        'year'   => $this->year,
        'isbn'   => $this->isbn,
        // La chiave "notes" compare solo se l'utente è autenticato
        'notes'  => $this->when($request->user() !== null, fn () => $this->internal_notes),
    ];
}

Passando una funzione anonima come secondo argomento, ne rimandiamo la valutazione: viene eseguita soltanto quando la condizione è vera, evitando calcoli inutili. Se la condizione è falsa, la chiave notes è del tutto assente dalla risposta, non presente con valore nullo.

Rappresentare le relazioni

Immaginiamo che ogni libro appartenga a una casa editrice, modellata da una relazione Eloquent publisher. Vogliamo includere i dati dell'editore nella risposta, ma solo quando la relazione è stata effettivamente caricata, per non provocare interrogazioni impreviste al database. L'helper whenLoaded serve esattamente a questo.

public function toArray(Request $request): array
{
    return [
        'id'     => $this->id,
        'title'  => $this->title,
        'author' => $this->author,
        // Include l'editore solo se la relazione è già stata caricata con with()
        'publisher' => PublisherResource::make($this->whenLoaded('publisher')),
    ];
}

Nel controller caricheremo la relazione in modo esplicito con l'eager loading, evitando il classico problema delle interrogazioni N+1.

// Carica in anticipo la relazione publisher per tutti i libri
$books = Book::query()->with('publisher')->latest()->get();

Paginare le collezioni

Restituire tutti i libri in un colpo solo non è sostenibile quando la tabella cresce. Eloquent offre la paginazione con il metodo paginate, che divide i risultati in pagine e legge il numero di pagina desiderato dalla query string. La cosa notevole è che le API Resource riconoscono un risultato paginato e aggiungono automaticamente i metadati di navigazione.

public function index()
{
    // Quindici libri per pagina; la pagina si sceglie con ?page=N
    $books = Book::query()->latest()->paginate(15);

    return BookResource::collection($books);
}

La risposta ora contiene, accanto alla chiave data con l'elenco dei libri, le chiavi links e meta con le informazioni di paginazione.

{
  "data": [ ... ],
  "links": {
    "first": "http://localhost:8000/api/books?page=1",
    "last":  "http://localhost:8000/api/books?page=4",
    "prev":  null,
    "next":  "http://localhost:8000/api/books?page=2"
  },
  "meta": {
    "current_page": 1,
    "from": 1,
    "last_page": 4,
    "per_page": 15,
    "to": 15,
    "total": 60
  }
}

Il client ha così tutto ciò che serve per navigare fra le pagine, senza che noi abbiamo scritto una sola riga per calcolare questi valori. Se vogliamo lasciare al client la scelta della dimensione della pagina, possiamo leggerla dalla richiesta imponendo però un tetto massimo, per evitare che qualcuno chieda un milione di elementi.

// Dimensione della pagina scelta dal client, ma con un limite di sicurezza
$perPage = min((int) $request->query('per_page', 15), 100);
$books = Book::query()->latest()->paginate($perPage);

Risorse per le collezioni con metadati personalizzati

Quando serve aggiungere metadati propri, oltre a quelli di paginazione, si può creare una classe dedicata alla collezione con make:resource e l'opzione --collection, oppure aggiungere la chiave with alla risorsa. Ecco come inserire un blocco informativo fisso in ogni risposta.

public function with(Request $request): array
{
    // Metadati aggiunti a ogni risposta prodotta da questa risorsa
    return [
        'meta' => [
            'api_version' => 'v1',
        ],
    ];
}

Conclusione

Le nostre risposte hanno ora una forma professionale e sotto controllo. Con le API Resource abbiamo separato il contratto pubblico dell'API dalla struttura interna del database, formattato i campi in modo esplicito, incluso dati in modo condizionale con when e whenLoaded e gestito la paginazione con i suoi metadati automatici. Il client riceve JSON progettato per lui, e il nostro schema resta libero di evolvere senza rompere l'interfaccia.

Restano però ancora aperte le porte a dati malformati: continuiamo ad accettare qualsiasi cosa arrivi nel corpo della richiesta. Nella prossima puntata chiuderemo questa falla introducendo la validazione con le Form Request, che rifiuteranno le richieste non valide con risposte di errore chiare e strutturate.