API Resources in Laravel: trasformare i modelli Eloquent in risposte JSON
Quando si costruisce un'API in Laravel, una delle sfide principali è disaccoppiare la rappresentazione dei dati dalla struttura interna del database. Restituire direttamente un modello Eloquent con response()->json($model) funziona nei prototipi, ma in produzione espone colonne sensibili, accoppia rigidamente il client allo schema delle tabelle e rende difficile evolvere l'API senza introdurre breaking changes. Le API Resources, introdotte in Laravel 5.5 e oggi parte integrante del framework, risolvono esattamente questo problema: agiscono come uno strato di trasformazione tra i modelli Eloquent e la risposta JSON inviata al client.
Che cosa è una API Resource
Una Resource è una classe che eredita da Illuminate\Http\Resources\Json\JsonResource e che incapsula la logica di serializzazione di un singolo modello. Il suo unico compito è ricevere un'istanza del modello, accedere ai suoi attributi e produrre un array che verrà poi convertito in JSON da Laravel. Il vantaggio principale è duplice: da un lato si ottiene un controllo granulare su quali campi vengono esposti e in quale formato, dall'altro si centralizza in un unico punto la rappresentazione esterna del dato, evitando la duplicazione di logica nei vari controller.
Accanto a JsonResource, Laravel fornisce ResourceCollection, una classe pensata per serializzare collezioni di modelli e che permette di aggiungere metadati (paginazione, link, statistiche aggregate) attorno all'elenco degli elementi.
Creazione di una Resource
La generazione di una Resource avviene tramite Artisan. Supponiamo di avere un modello Post con i campi id, title, slug, body, excerpt, published_at e le relazioni author e comments:
php artisan make:resource PostResource
php artisan make:resource PostCollection
Il primo comando crea la classe App\Http\Resources\PostResource, che gestirà la serializzazione di un singolo post. Il secondo crea App\Http\Resources\PostCollection, dedicata alla serializzazione di una lista paginata di post.
Anatomia di una Resource
Il cuore di ogni Resource è il metodo toArray(), che riceve la richiesta HTTP corrente e restituisce un array associativo. All'interno della classe, $this agisce come proxy verso il modello sottostante, quindi è possibile accedere agli attributi del modello come se si fosse all'interno del modello stesso:
<?php
namespace App\Http\Resources;
use Illuminate\Http\Request;
use Illuminate\Http\Resources\Json\JsonResource;
class PostResource extends JsonResource
{
/**
* Trasforma la risorsa in un array.
*
* @return array<string, mixed>
*/
public function toArray(Request $request): array
{
return [
'id' => $this->id,
'title' => $this->title,
'slug' => $this->slug,
'excerpt' => $this->excerpt,
'body' => $this->body,
'published_at' => $this->published_at?->toIso8601String(),
'created_at' => $this->created_at->toIso8601String(),
'updated_at' => $this->updated_at->toIso8601String(),
];
}
}
In questo esempio si nota subito un primo vantaggio: il campo published_at viene normalizzato nel formato ISO 8601, indipendentemente da come è memorizzato nel database. Allo stesso modo, eventuali colonne interne come internal_notes o deleted_at semplicemente non compaiono nell'array restituito e quindi non vengono mai esposte al client.
Utilizzo nel controller
Una Resource viene tipicamente istanziata all'interno di un controller, passando il modello al costruttore. Per restituire una collezione si utilizza invece il metodo statico collection():
<?php
namespace App\Http\Controllers\Api;
use App\Http\Controllers\Controller;
use App\Http\Resources\PostResource;
use App\Models\Post;
use Illuminate\Http\Request;
class PostController extends Controller
{
public function index(Request $request)
{
// Recupera i post paginati con eager loading delle relazioni
$posts = Post::with(['author', 'comments'])
->whereNotNull('published_at')
->orderByDesc('published_at')
->paginate(15);
return PostResource::collection($posts);
}
public function show(Post $post)
{
// Carica le relazioni solo quando si visualizza il singolo post
$post->load(['author', 'comments.user']);
return new PostResource($post);
}
}
Quando si passa un paginatore al metodo collection(), Laravel aggiunge automaticamente i metadati di paginazione (links e meta) alla risposta JSON, senza necessità di codice aggiuntivo.
Wrapping della risposta
Per impostazione predefinita, Laravel avvolge le risposte delle Resource all'interno di una chiave data. Questa scelta segue convenzioni diffuse come JSON:API ed è utile per consentire l'aggiunta di metadati di primo livello senza confliggere con i dati. Per una richiesta a /api/posts/1 la risposta avrà la forma:
{
"data": {
"id": 1,
"title": "Introduzione alle API Resources",
"slug": "introduzione-alle-api-resources",
"excerpt": "...",
"body": "...",
"published_at": "2026-05-10T09:30:00+00:00",
"created_at": "2026-05-10T09:00:00+00:00",
"updated_at": "2026-05-10T09:30:00+00:00"
}
}
Se si desidera disabilitare il wrapping globalmente (sconsigliato in API pubbliche, ma utile in alcuni contesti interni), si può chiamare JsonResource::withoutWrapping() all'interno di un service provider:
<?php
namespace App\Providers;
use Illuminate\Http\Resources\Json\JsonResource;
use Illuminate\Support\ServiceProvider;
class AppServiceProvider extends ServiceProvider
{
public function boot(): void
{
// Disabilita il wrapping in "data" per tutte le Resource
JsonResource::withoutWrapping();
}
}
In alternativa, si può sovrascrivere la proprietà $wrap in una specifica Resource per usare una chiave diversa, ad esempio post.
Inclusione condizionale di attributi
Spesso è necessario esporre alcuni campi solo a determinati utenti o in determinate situazioni. Le Resource forniscono metodi helper espressivi per gestire questi casi senza ricorrere a if annidati. Il più comune è when(), che include un attributo solo se la condizione è vera:
public function toArray(Request $request): array
{
return [
'id' => $this->id,
'title' => $this->title,
'slug' => $this->slug,
'body' => $this->body,
// L'email dell'autore è visibile solo agli amministratori
'author_email' => $this->when(
$request->user()?->isAdmin(),
fn () => $this->author->email
),
// Le statistiche di lettura vengono incluse solo se richieste
'reading_stats' => $this->when(
$request->boolean('include_stats'),
fn () => [
'views' => $this->views_count,
'avg_duration' => $this->avg_reading_duration,
]
),
];
}
Il metodo when() accetta come secondo argomento una closure, valutata solo se la condizione è vera. Questo è importante quando il valore da restituire richiede operazioni costose o accessi a relazioni non ancora caricate.
Per raggruppare più attributi condizionali sotto la stessa condizione, si utilizza mergeWhen(), che evita di ripetere lo stesso controllo più volte:
public function toArray(Request $request): array
{
return [
'id' => $this->id,
'title' => $this->title,
// Tutti questi campi sono visibili solo all'autore del post
$this->mergeWhen($request->user()?->id === $this->author_id, [
'draft_notes' => $this->draft_notes,
'revision_count' => $this->revision_count,
'last_saved_at' => $this->last_saved_at?->toIso8601String(),
]),
];
}
Gestione delle relazioni con whenLoaded
Uno degli errori più frequenti nelle API costruite con Laravel è il problema N+1: per ogni elemento di una collezione viene eseguita una query aggiuntiva per caricare le relazioni. Le Resource non risolvono il problema da sole (è sempre necessario fare eager loading nel controller), ma offrono il metodo whenLoaded() per includere una relazione solo quando è effettivamente già stata caricata in memoria, evitando query implicite:
<?php
namespace App\Http\Resources;
use Illuminate\Http\Request;
use Illuminate\Http\Resources\Json\JsonResource;
class PostResource extends JsonResource
{
public function toArray(Request $request): array
{
return [
'id' => $this->id,
'title' => $this->title,
'slug' => $this->slug,
'body' => $this->body,
// L'autore viene incluso solo se la relazione è già stata caricata
'author' => new AuthorResource($this->whenLoaded('author')),
// I commenti vengono inclusi come collezione, sempre solo se eager-loaded
'comments' => CommentResource::collection(
$this->whenLoaded('comments')
),
// Conteggio dei commenti tramite withCount
'comments_count' => $this->whenCounted('comments'),
];
}
}
Il metodo whenCounted(), complementare a whenLoaded(), funziona in tandem con withCount() di Eloquent: nel controller si scriverà Post::withCount('comments')->get() e il valore comparirà nella risposta solo quando effettivamente calcolato.
Resource collection personalizzate
La classe PostCollection generata da Artisan permette di personalizzare la struttura della risposta quando si serve una collezione di post. È utile quando si vogliono aggiungere informazioni aggregate o metadati specifici dell'endpoint:
<?php
namespace App\Http\Resources;
use Illuminate\Http\Request;
use Illuminate\Http\Resources\Json\ResourceCollection;
class PostCollection extends ResourceCollection
{
/**
* Trasforma la collezione di risorse in un array.
*
* @return array<string, mixed>
*/
public function toArray(Request $request): array
{
return [
'data' => $this->collection,
'meta' => [
'total_published' => $this->collection->whereNotNull('published_at')->count(),
'oldest_publication' => $this->collection->min('published_at'),
'newest_publication' => $this->collection->max('published_at'),
],
];
}
}
Per utilizzarla nel controller è sufficiente istanziarla con il risultato della query:
public function index(Request $request)
{
$posts = Post::with('author')->paginate(20);
return new PostCollection($posts);
}
Aggiungere metadati con additional e with
Esistono due meccanismi per aggiungere dati di primo livello alla risposta, oltre alla chiave data. Il metodo additional(), chiamato sull'istanza della Resource, permette di aggiungere metadati per una singola risposta:
public function show(Post $post)
{
$post->load('author');
return (new PostResource($post))->additional([
'meta' => [
'api_version' => '2.0',
'cached_at' => now()->toIso8601String(),
],
]);
}
Il metodo with(), definito all'interno della classe Resource, permette invece di aggiungere metadati che saranno presenti in tutte le risposte di quella Resource:
public function with(Request $request): array
{
return [
'meta' => [
'api_version' => config('app.api_version'),
'documentation' => url('/docs/api'),
],
];
}
Personalizzare la risposta HTTP
A volte è necessario controllare anche aspetti della risposta HTTP che esulano dal corpo JSON, come lo status code o gli header. Il metodo withResponse() riceve la richiesta e la risposta in costruzione e permette di modificarla prima dell'invio:
use Illuminate\Http\JsonResponse;
use Illuminate\Http\Request;
public function withResponse(Request $request, JsonResponse $response): void
{
$response->header('X-Resource-Type', 'post');
$response->header('Cache-Control', 'public, max-age=300');
}
Per controllare lo status code in scenari come la creazione di una risorsa, è invece più chiaro restituire la Resource direttamente con response():
public function store(StorePostRequest $request)
{
$post = Post::create($request->validated());
return (new PostResource($post))
->response()
->setStatusCode(201)
->header('Location', route('posts.show', $post));
}
Annidare le Resource
Una delle caratteristiche più potenti è la possibilità di comporre Resource tra loro, in modo che ogni modello abbia la propria classe di serializzazione e la struttura della risposta rispecchi naturalmente quella del dominio. Ipotizziamo di avere anche AuthorResource e CommentResource:
<?php
namespace App\Http\Resources;
use Illuminate\Http\Request;
use Illuminate\Http\Resources\Json\JsonResource;
class AuthorResource extends JsonResource
{
public function toArray(Request $request): array
{
return [
'id' => $this->id,
'name' => $this->name,
'avatar_url' => $this->avatar_url,
'bio' => $this->bio,
];
}
}
class CommentResource extends JsonResource
{
public function toArray(Request $request): array
{
return [
'id' => $this->id,
'body' => $this->body,
'created_at' => $this->created_at->toIso8601String(),
'user' => new AuthorResource($this->whenLoaded('user')),
];
}
}
Composte insieme, queste Resource producono risposte gerarchiche che rispettano la struttura del dominio. La chiave è sempre usare whenLoaded() per evitare query implicite e gestire l'eager loading correttamente nel controller.
Inclusione dinamica delle relazioni
Un pattern comune nelle API moderne è permettere al client di scegliere quali relazioni includere tramite un parametro di query, ad esempio ?include=author,comments. Si può implementare questo comportamento combinando whenLoaded() con un caricamento condizionale nel controller:
public function index(Request $request)
{
// Lista delle relazioni che il client può richiedere
$allowedIncludes = ['author', 'comments', 'comments.user', 'tags'];
// Filtra solo le relazioni effettivamente consentite
$includes = array_intersect(
explode(',', $request->input('include', '')),
$allowedIncludes
);
$query = Post::query();
if (!empty($includes)) {
$query->with($includes);
}
$posts = $query->paginate(15);
return PostResource::collection($posts);
}
In questo modo la Resource resta agnostica rispetto a cosa è stato caricato: se author non è in $includes, la relazione non viene caricata e whenLoaded() ne ometterà la chiave dalla risposta.
Resource per richieste non Eloquent
Anche se sono nate per i modelli Eloquent, le Resource funzionano con qualsiasi oggetto o array. Possono quindi essere utilizzate per serializzare risposte di servizi esterni, DTO o strutture costruite a runtime. In questi casi è sufficiente accedere agli attributi tramite $this->resource, che contiene l'oggetto originale passato al costruttore:
<?php
namespace App\Http\Resources;
use Illuminate\Http\Request;
use Illuminate\Http\Resources\Json\JsonResource;
class WeatherResource extends JsonResource
{
public function toArray(Request $request): array
{
// $this->resource contiene l'array o l'oggetto passato al costruttore
return [
'location' => $this->resource['city'],
'temperature' => round($this->resource['temp_celsius'], 1),
'humidity' => $this->resource['humidity_percent'],
'measured_at' => $this->resource['timestamp'],
];
}
}
Considerazioni su performance e best practice
Per quanto le Resource siano leggere, alcune accortezze fanno la differenza in produzione. È fondamentale fare sempre eager loading delle relazioni nei controller e usare whenLoaded() nelle Resource: la combinazione previene query N+1 senza obbligare ogni endpoint a caricare tutte le relazioni possibili. Le date dovrebbero essere sempre serializzate in un formato standard come ISO 8601, per evitare ambiguità di timezone lato client. Per i campi numerici sensibili (importi monetari, coordinate geografiche) conviene normalizzare il tipo nella Resource, ad esempio convertendo stringhe in float o applicando arrotondamenti coerenti.
Quando una Resource cresce e contiene molta logica di formattazione, è un buon segnale di estrazione: spostare la logica in metodi privati della stessa classe, oppure in casts personalizzati sul modello, mantiene toArray() leggibile. È buona pratica, infine, non includere logica di autorizzazione nelle Resource: i controlli di accesso devono essere delegati a middleware, policy o form request. Le Resource si limitano a esporre o nascondere campi sulla base del contesto già autorizzato.
Conclusione
Le API Resources rappresentano il modo idiomatico di costruire risposte JSON in Laravel: separano la rappresentazione esterna dei dati dalla struttura interna del database, centralizzano la logica di serializzazione, supportano la composizione gerarchica tra modelli e offrono strumenti espliciti per la gestione delle relazioni e l'inclusione condizionale di attributi. Adottarle fin dall'inizio di un progetto significa avere un'API stabile, prevedibile e facilmente evolvibile nel tempo, con un livello di disaccoppiamento tra dominio e contratto pubblico che difficilmente si raggiunge restituendo direttamente modelli Eloquent.