DEV Community

Cover image for Laravel Doesn’t Need GraphQL Part 2: Use Components with Props for Exact Data
Raheel Shan
Raheel Shan

Posted on • Originally published at raheelshan.com

Laravel Doesn’t Need GraphQL Part 2: Use Components with Props for Exact Data

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();
    }
}
Enter fullscreen mode Exit fullscreen mode

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();
    }
}
Enter fullscreen mode Exit fullscreen mode

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
}
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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 }}');
    }
}
Enter fullscreen mode Exit fullscreen mode

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');
    }
}
Enter fullscreen mode Exit fullscreen mode

You already define fields in the component. Just need to make sure our fields are similar with the eloquent model.

Step 2 - Use in queries

$users = User::select(UserCard::toArray())->get();
Enter fullscreen mode Exit fullscreen mode
  • 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);
Enter fullscreen mode Exit fullscreen mode

Or return HTML fragments directly:

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

The frontend just injects the ready-made HTML into the DOM—no need to parse JSON.

Why Components Work Better Than GraphQL in Laravel

  • No extra layer—no schemas, resolvers, or GraphQL servers.

  • Familiar workflow—still using controllers, Eloquent, and 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.

Support my work

Top comments (0)