API Resources vs DTOs in Laravel: a practical comparison

Understanding when API Resources shine and when DTOs provide superior control for complex data transformations
05.12.2025 — Marc Bernet — 7 min read

Laravel's API Resources are the de facto standard for API response transformation. They're well-documented, officially supported, and effective for straightforward CRUD operations. However, as application complexity increases and query patterns evolve, the limitations of this approach become apparent.

This article examines when API Resources are appropriate and when Data Transfer Objects (DTOs) provide superior architectural benefits.

The standard case: API Resources are sufficient

Consider a typical user CRUD endpoint where the response closely mirrors the underlying model:

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

Benefits: minimal boilerplate, rapid implementation, reusable across endpoints.

The N+1 problem: a common pitfall

As requirements expand to include relational data, a common antipattern emerges:

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

This implementation triggers additional queries for each entity. With 50 users, this results in 100+ database queries.

The canonical solution: eager loading in the controller:

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

However, this approach introduces a separation of concerns issue: the Resource freely accesses relationships while the responsibility for query optimization resides in the controller. Forgetting the with() clause results in performance degradation that may not be immediately apparent.

Complex aggregations: where Resources show limitations

Consider an endpoint requiring aggregated data from multiple tables:

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

This query returns stdClass objects rather than Eloquent models. Adapting Resources for this use case requires compromises:

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

While functional, this approach sacrifices type safety and introduces ambiguity regarding data structure expectations.

DTOs: an alternative approach

Data Transfer Objects provide explicit structure and decouple data retrieval from transformation logic:

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

Advantages: Complete type safety with PHP 8+ typed properties, explicit data structure contracts, elimination of accidental N+1 queries, independent testability of transformation logic, and reusability across different application layers (jobs, events, commands).

Comparative analysis

API Resources: Optimal Use Cases

Simple CRUD operations, response structure closely mirrors model structure, teams with strong Laravel framework knowledge, and small to medium-scale applications.

DTOs: Preferred Scenarios

Complex queries involving joins and aggregations, multi-source data composition, performance-critical APIs, strict contract enforcement requirements, and comprehensive test coverage needs.

Architectural recommendation

Rather than adopting a monolithic approach, select the appropriate pattern based on specific endpoint requirements:

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

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

Indicators for DTO adoption: Use of selectRaw() or DB::raw() in queries, three or more table joins, significant divergence between response structure and model, recurring N+1 query issues, and requirements for isolated transformation logic testing.

Conclusion

While API Resources represent the framework's recommended approach and function effectively for standard use cases, Laravel's philosophy does not mandate their universal application. DTOs offer superior control and architectural clarity in scenarios involving complex data transformations.

Optimal software design prioritizes problem-specific solutions over dogmatic pattern adherence. The choice between Resources and DTOs should be driven by the specific characteristics of each endpoint rather than conventional practice.

💡 How much does it cost to develop an app for your business? Discover the prices