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');
}
}
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();
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]);
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.
Top comments (0)