Use Laravel Eloquent API Resources to Shape JSON Responses Safely
Use Laravel Eloquent API Resources to explicitly shape JSON output, avoid exposing sensitive columns, and prevent N+1 queries in collections.
26 May 2026, 05:09 UTC

The problem: models expose too much, and collections trigger hidden queries
Returning an Eloquent model directly from a controller serializes every accessible attribute, including columns you may not want to send to the client, such as password hashes or internal flags. When a controller returns a collection, touching a relationship inside a transformation can run one query per row, the classic N+1 problem.
Useful takeaway: create an Eloquent API Resource to explicitly choose which attributes and relationships appear in JSON. Use when for conditional fields and whenLoaded for relationships that have already been eager loaded. Verify the output with an HTTP client and query count with Telescope or Clockwork.
Version assumption and setup
This guide assumes Laravel 9.x or later. Confirm your installation with:
php artisan --version
Run commands from the Laravel project root. The user account needs write access to app/Http/Resources.
Generate a resource
php artisan make:resource UserResource
Expected result: Laravel creates app/Http/Resources/UserResource.php. Risk: if a class with that name already exists, the command overwrites it unless you choose a different name.
Worked example: transform a user model
The following resource maps only selected attributes and conditionally includes a role and posts relationship.
namespace App\Http\Resources;
use Illuminate\Http\Request;
use Illuminate\Http\Resources\Json\JsonResource;
class UserResource extends JsonResource
{
public function toArray(Request $request): array
{
return [
'id' => $this->id,
'name' => $this->name,
'email' => $this->email,
'role' => $this->when($this->role !== null, $this->role),
'posts' => $this->whenLoaded('posts'),
];
}
}
when includes the role key only when the condition is true. whenLoaded includes posts only if the relationship was already eager loaded; otherwise the relationship is not included and no query is triggered by the resource itself.
Return the resource from a controller
use App\Http\Resources\UserResource;
use App\Models\User;
public function show(User $user): UserResource
{
return new UserResource($user);
}
Route model binding resolves $user before the controller runs. The resource transforms the model into the array defined above, and Laravel converts that array to JSON.
Transform a paginated collection
use App\Http\Resources\UserResource;
use App\Models\User;
public function index()
{
return UserResource::collection(User::paginate());
}
UserResource::collection wraps every model in the page with the same transformation. Laravel automatically adds pagination metadata such as current_page, last_page, and data.
Avoid N+1 queries with eager loading
To include posts without triggering a query per user, eager load the relationship before the resource runs:
public function index()
{
return UserResource::collection(User::with('posts')->paginate());
}
Because the resource uses whenLoaded('posts'), it reads the already-loaded relationship. If you removed with('posts') and accessed $this->posts directly in toArray, Laravel would execute one additional query for every user in the collection.
Practical verification
- Open the endpoint in an HTTP client or browser developer tools and confirm the JSON contains only the keys defined in
toArray. - Open Telescope or Clockwork and compare the query count for a collection endpoint with and without
with('posts'). - Confirm that sensitive columns such as
passworddo not appear in the response body.
Do not treat the absence of a key in one response as proof that the field is never sent; check multiple records and conditions.
Limits and common mistakes
- Resources are a presentation layer. Do not put business logic, accessor calculations, or service calls inside
toArray; keep those in models, accessors, or service classes. whenLoadeddoes not eager load a relationship. It only reads a relationship that is already loaded. If you need the data, eager load it in the controller or query.- Returning
User::find($id)directly bypasses the resource and may expose hidden attributes. - Overusing
whenwith database queries inside the callback can move the N+1 problem into the transformation layer.
Undoing the generated file
Because make:resource only creates a PHP class, reverting it means deleting app/Http/Resources/UserResource.php and any controller references to it. No database migration is involved.
Review note
Verify the exact make:resource output and resource method signatures against your installed Laravel version before adopting this pattern in production.
0 replies
A thoughtful contribution can make all the difference. Be the first to share one.