API Resources vs DTOs a Laravel: una comparació pràctica

Entenent quan API Resources brillen i quan els DTOs ofereixen un control superior per transformacions de dades complexes
05.12.2025 — Marc Bernet — 8 min read

Els API Resources de Laravel són l'estàndard de facto per a la transformació de respostes API. Estan ben documentats, tenen suport oficial i són efectius per operacions CRUD senzilles. No obstant això, a mesura que augmenta la complexitat de l'aplicació i evolucionen els patrons de consulta, les limitacions d'aquest enfocament es fan evidents.

Aquest article examina quan els API Resources són apropiats i quan els Data Transfer Objects (DTOs) proporcionen beneficis arquitectònics superiors.

El cas estàndard: API Resources són suficients

Considera un endpoint CRUD típic d'usuaris on la resposta reflecteix fidelment el model subjacent:

// app/Http/Resources/UserResource.php
class UserResource extends JsonResource
{
    public function toArray($request)
    {
        return [
            'id' => $this->id,
            'name' => $this->name,
            'email' => $this->email,
            'created_at' => $this->created_at->format('Y-m-d'),
        ];
    }
}

// Controller
public function index()
{
    $users = User::all();
    return UserResource::collection($users);
}

Beneficis: codi mínim, implementació ràpida, reutilitzable en múltiples endpoints.

El problema N+1: un error comú

A mesura que els requisits s'expandeixen per incloure dades relacionals, emergeix un antipatró comú:

class UserResource extends JsonResource
{
    public function toArray($request)
    {
        return [
            'id' => $this->id,
            'name' => $this->name,
            'posts_count' => $this->posts->count(), // 🚨 N+1 query
            'last_comment' => $this->comments()->latest()->first(), // 🚨 N+1 query
        ];
    }
}

Aquesta implementació dispara consultes addicionals per cada entitat. Amb 50 usuaris, això resulta en més de 100 consultes a la base de dades.

La solució canònica: eager loading al controlador:

$users = User::with(['posts', 'comments'])->get();

No obstant això, aquest enfocament introdueix un problema de separació de responsabilitats: el Resource accedeix lliurement a les relacions mentre la responsabilitat d'optimitzar les consultes resideix al controlador. Oblidar la clàusula with() resulta en degradació del rendiment que pot no ser immediatament evident.

Agregacions complexes: on els Resources mostren limitacions

Considera un endpoint que requereix dades agregades de múltiples taules:

$stats = DB::table('users')
    ->join('posts', 'users.id', '=', 'posts.user_id')
    ->join('comments', 'posts.id', '=', 'comments.post_id')
    ->selectRaw('
        users.id,
        users.name,
        COUNT(DISTINCT posts.id) as posts_count,
        COUNT(comments.id) as comments_count,
        MAX(posts.created_at) as last_post_date
    ')
    ->groupBy('users.id', 'users.name')
    ->get();

Aquesta consulta retorna objectes stdClass en lloc de models Eloquent. Adaptar Resources per aquest cas d'ús requereix compromisos:

class UserStatsResource extends JsonResource
{
    public function toArray($request)
    {
        return [
            'id' => $this->id ?? null,
            'name' => $this->name ?? null,
            'posts_count' => $this->posts_count ?? 0,
            'comments_count' => $this->comments_count ?? 0,
        ];
    }
}

Tot i que funcional, aquest enfocament sacrifica la seguretat de tipus i introdueix ambigüitat sobre les expectatives d'estructura de dades.

DTOs: un enfocament alternatiu

Els Data Transfer Objects proporcionen estructura explícita i desacoblen l'obtenció de dades de la lògica de transformació:

// app/DTOs/UserStatsDTO.php
class UserStatsDTO
{
    public function __construct(
        public int $id,
        public string $name,
        public int $postsCount,
        public int $commentsCount,
        public ?string $lastPostDate
    ) {}

    public static function fromQueryResult(object $result): self
    {
        return new self(
            id: $result->id,
            name: $result->name,
            postsCount: $result->posts_count,
            commentsCount: $result->comments_count,
            lastPostDate: $result->last_post_date
        );
    }

    public function toArray(): array
    {
        return [
            'id' => $this->id,
            'name' => $this->name,
            'posts_count' => $this->postsCount,
            'comments_count' => $this->commentsCount,
            'last_post_date' => $this->lastPostDate,
        ];
    }
}

// Controller
public function stats()
{
    $results = DB::table('users')
        ->join('posts', 'users.id', '=', 'posts.user_id')
        ->selectRaw('...')
        ->get();

    $dtos = $results->map(fn($r) => UserStatsDTO::fromQueryResult($r));
    
    return response()->json($dtos->map->toArray());
}

Avantatges: Seguretat de tipus completa amb propietats tipades de PHP 8+, contractes explícits d'estructura de dades, eliminació de consultes N+1 accidentals, testabilitat independent de la lògica de transformació, i reutilització en diferents capes de l'aplicació (jobs, events, comandaments).

Anàlisi comparativa

API Resources: casos d'ús òptims

Operacions CRUD simples, estructura de resposta que reflecteix fidelment l'estructura del model, equips amb fort coneixement del framework Laravel, i aplicacions de petita a mitjana escala.

DTOs: escenaris preferits

Consultes complexes amb joins i agregacions, composició de dades de múltiples fonts, APIs crítiques en rendiment, requisits estrictes de contractes, i necessitats de cobertura completa de tests.

Recomanació arquitectònica

En lloc d'adoptar un enfocament monolític, selecciona el patró apropiat segons els requisits específics de cada endpoint:

// CRUD simple → API Resource
public function show(User $user)
{
    return new UserResource($user->load('profile'));
}

// Agregació complexa → DTO
public function dashboard()
{
    $stats = $this->calculateDashboardStats();
    return response()->json(
        DashboardDTO::fromStats($stats)->toArray()
    );
}

Indicadors per adoptar DTOs: Ús de selectRaw() o DB::raw() en consultes, tres o més joins de taules, divergència significativa entre estructura de resposta i model, problemes recurrents de N+1, i requisits per testar la lògica de transformació de forma aïllada.

Conclusió

Tot i que els API Resources representen l'enfocament recomanat pel framework i funcionen efectivament per casos d'ús estàndard, la filosofia de Laravel no obliga a la seva aplicació universal. Els DTOs ofereixen control superior i claredat arquitectònica en escenaris que involucren transformacions de dades complexes.

El disseny òptim de programari prioritza solucions específiques al problema sobre l'adherència dogmàtica a patrons. L'elecció entre Resources i DTOs ha d'estar guiada per les característiques específiques de cada endpoint en lloc de la pràctica convencional.

💡 Quant costa desenvolupar una app per al teu negoci? Descobreix els preus