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.
