GraphQL often gets attention because of its ability to let the frontend decide what data it needs. That means you send a query, pass it through a GraphQL layer, and get back exactly the fields you requested. It’s flexible, but it also introduces overhead: you need to learn GraphQL’s schema, rules, and conventions, and your backend code now lives in a world that isn’t quite Laravel anymore.

What if you could stay fully Laravel and still achieve the same control over data exposure? That’s where components come in.

The Idea: Components with Properties as DTOs

In a typical Laravel app, Blade components already act like small, reusable building blocks. They receive data, enforce structure, and return a view. But we can extend their role even further:

  • Treat a component’s properties as the fields we want to expose.
  • Instead of creating a dedicated DTO class for every API or Blade partial, use the component itself to declare which model properties are allowed in its context.
  • When rendering APIs, you can return a component the same way you would return a Blade view—except here, the component’s props define the data contract.

This means each component is:

  • A view for Blade rendering.
  • A structured response for API rendering.
  • A natural replacement for DTOs without adding new layers.

How It Works in Practice

First thing first. We need to make 2 traits to make everything work for this setup. Let's start.

  1. The ResponsableComponent Trait  

This will return either json or html.

                                namespace App\Traits;

use Illuminate\Contracts\Support\Responsable;

trait ResponsableComponent
{
    public function toResponse($request)
    {
        if ($request->wantsJson()) {
            return response()->json($this->toArray());
        }

        return $this->render();
    }
}
                            

2. The ArraySerializableComponent Trait

This automatically converts all public properties of the component into an array.

                                namespace App\Traits;

trait ArraySerializableComponent
{
    public static function toArray(): array
    {
        $vars = get_object_vars($this);

        // Remove internal Laravel properties like `componentName`
        return collect($vars)
            //->reject(fn($v, $k) => str_starts_with($k, '__'))
            ->all();
    }
}

                            

3. Using Them in a Component

Now a component can look minimal:

                                namespace App\View\Components;

use Illuminate\View\Component;
use App\Traits\ResponsableComponent;
use App\Traits\ArraySerializableComponent;

class UserCard extends Component
{
    use ResponsableComponent, ArraySerializableComponent;

    // other code
}

                            

4. Auto-Injecting Traits on Component Creation

To avoid manual imports every time, you can extend the Artisan command:

1. Publish the stub:

                                php artisan stub:publish --tag=component.stub
                            

2. Open stubs/component.stub and update it:

                                class {{ class }} extends Component
{
    use App\Traits\ResponsableComponent;
    use App\Traits\ArraySerializableComponent;

    /**
     * Create a new component instance.
     *
     * @return void
     */
    public function __construct()
    {
        //
    }

    /**
     * Get the view / contents that represent the component.
     */
    public function render(): View|Closure|string
    {
        return view('components.{{ view }}');
    }
}
                            

Now every php artisan make:component UserCard will already include your two traits. 🎉

Now that our setup is ready, we can move on.

Step 1 - Declare fields

                                namespace App\View\Components;

use Illuminate\View\Component;
use App\Traits\ResponsableComponent;
use App\Traits\ArraySerializableComponent;

class UserCard extends Component
{
    use ResponsableComponent, ArraySerializableComponent;

    public string $name;
    public string $email;

    public function __construct(
        public string $name,
        public string $email,
    ) {}

    public function render()
    {
        return view('components.user-card');
    }
}
                            

Step 2 - Use in queries

                                    $users = User::select(UserCard::toArray())->get();

                            
  • The toArray() method enforces which columns must be selected.
  • Even if your User model has 20 columns, you’ll only fetch name and email.
  • That’s GraphQL precision with a one-liner.

Step 3 - Flexible Responses

GraphQL always responds in JSON. With this approach, you’re not locked in:

Return JSON for API endpoints:

                                return response()->json($users);
                            

Or return HTML fragments directly:

                                return view('user.index', ['users' => $users]);
                            

Why Components Work Better Than GraphQL in Laravel

  • No extra layer—no schemas, resolvers, or GraphQL servers.
  • Familiar workflow—still using controllers, Eloquent, Blade.
  • Strict contracts—component properties define exactly what’s selected.
  • Dual purpose—works as DTOs for JSON or as Blade for HTML.
  • Laravel native—developers feel at home; no new language required.

Caveats

Components as contracts are simple on purpose. They’re meant to behave like DTOs: strict props in, exact columns out. If you need something dynamic—like a computed field or conditional prop—you can still handle it inside the component the same way you already would.

Performance-wise, don’t overthink it. If a page has a lot of components, of course the database is going to be queried. The difference is that each query is leaner because you’ve defined the exact columns up front. That’s not a problem; that’s optimization.

As with DTOs, this approach shines best on fresh builds. Legacy apps can adopt it piece by piece, but don’t expect to retrofit it overnight. And no, I don’t have fancy benchmarks yet. I’d rather let the community try it in real apps and see what results come back.

Final Thoughts

Unlike GraphQL, Laravel components-as-contracts work both in APIs and Blade views. Whether you’re sending JSON or rendering HTML, the same component ensures you never over-fetch.


If you found this post helpful, consider supporting my work — it means a lot.

 Raheel Shan | Support my work
Raheel Shan | Support my work
Support my work
raheelshan.com
Comments


Comment created and will be displayed once approved.