API Resources vs DTOs en Laravel: una comparación práctica

Entendiendo cuándo API Resources brillan y cuándo los DTOs ofrecen un control superior para transformaciones de datos complejas
05.12.2025 — Marc Bernet — 8 min read

Los API Resources de Laravel son el estándar de facto para la transformación de respuestas API. Están bien documentados, tienen soporte oficial y son efectivos para operaciones CRUD sencillas. Sin embargo, a medida que aumenta la complejidad de la aplicación y evolucionan los patrones de consulta, las limitaciones de este enfoque se hacen evidentes.

Este artículo examina cuándo los API Resources son apropiados y cuándo los Data Transfer Objects (DTOs) proporcionan beneficios arquitectónicos superiores.

El caso estándar: API Resources son suficientes

Considera un endpoint CRUD típico de usuarios donde la respuesta refleja fielmente el modelo subyacente:

// 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);
}

Beneficios: código mínimo, implementación rápida, reutilizable en múltiples endpoints.

El problema N+1: un error común

A medida que los requisitos se expanden para incluir datos relacionales, emerge un antipatrón común:

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
        ];
    }
}

Esta implementación dispara consultas adicionales para cada entidad. Con 50 usuarios, esto resulta en más de 100 consultas a la base de datos.

La solución canónica: eager loading en el controlador:

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

Sin embargo, este enfoque introduce un problema de separación de responsabilidades: el Resource accede libremente a las relaciones mientras la responsabilidad de optimizar las consultas reside en el controlador. Olvidar la cláusula with() resulta en degradación del rendimiento que puede no ser inmediatamente evidente.

Agregaciones complejas: donde los Resources muestran limitaciones

Considera un endpoint que requiere datos agregados de múltiples tablas:

$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();

Esta consulta devuelve objetos stdClass en lugar de modelos Eloquent. Adaptar Resources para este caso de uso requiere 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,
        ];
    }
}

Aunque funcional, este enfoque sacrifica la seguridad de tipos e introduce ambigüedad sobre las expectativas de estructura de datos.

DTOs: un enfoque alternativo

Los Data Transfer Objects proporcionan estructura explícita y desacoplan la obtención de datos de la lógica de transformación:

// 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());
}

Ventajas: Seguridad de tipos completa con propiedades tipadas de PHP 8+, contratos explícitos de estructura de datos, eliminación de consultas N+1 accidentales, testabilidad independiente de la lógica de transformación, y reutilización en diferentes capas de la aplicación (jobs, eventos, comandos).

Análisis comparativo

API Resources: casos de uso óptimos

Operaciones CRUD simples, estructura de respuesta que refleja fielmente la estructura del modelo, equipos con fuerte conocimiento del framework Laravel, y aplicaciones de pequeña a mediana escala.

DTOs: escenarios preferidos

Consultas complejas con joins y agregaciones, composición de datos de múltiples fuentes, APIs críticas en rendimiento, requisitos estrictos de contratos, y necesidades de cobertura completa de tests.

Recomendación arquitectónica

En lugar de adoptar un enfoque monolítico, selecciona el patrón apropiado según los requisitos específicos de cada endpoint:

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

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

Indicadores para adoptar DTOs: Uso de selectRaw() o DB::raw() en consultas, tres o más joins de tablas, divergencia significativa entre estructura de respuesta y modelo, problemas recurrentes de N+1, y requisitos para testear la lógica de transformación de forma aislada.

Conclusión

Aunque los API Resources representan el enfoque recomendado por el framework y funcionan efectivamente para casos de uso estándar, la filosofía de Laravel no obliga a su aplicación universal. Los DTOs ofrecen control superior y claridad arquitectónica en escenarios que involucran transformaciones de datos complejas.

El diseño óptimo de software prioriza soluciones específicas al problema sobre la adherencia dogmática a patrones. La elección entre Resources y DTOs debe estar guiada por las características específicas de cada endpoint en lugar de la práctica convencional.

💡 ¿Cuánto cuesta desarrollar una app para tu negocio? Ver precios