Laravel API Resources: Fieldsets, Relations &amp; Versioning | Mohamed Said        [  ![Mohamed Said](https://cdn.msaied.com/01KT78WE565VEMM3PSNQAAB0MH.png)   Mohamed Said Laravel Backend Engineer  ](https://msaied.com) [ Home ](https://msaied.com) [ Projects ](https://msaied.com/projects) [ Articles  ](https://msaied.com/articles) [ Certificates ](https://msaied.com/certificates) [ Contact ](https://msaied.com#contact-section) 

       [  ](https://github.com/EG-Mohamed)       

 [ Home ](https://msaied.com) [ Projects ](https://msaied.com/projects) [ Articles ](https://msaied.com/articles) [ Certificates ](https://msaied.com/certificates) [ Contact ](https://msaied.com#contact-section) 

  [ home ](https://msaied.com)    [ articles ](https://msaied.com/articles)    Laravel API Resources: Sparse Fieldsets, Conditional Relationships, and Stable Versioning        On this page       1. [  Beyond toArray: Getting Serious About Laravel API Resources ](#beyond-codetoarraycode-getting-serious-about-laravel-api-resources)
2. [  Sparse Fieldsets Without a Package ](#sparse-fieldsets-without-a-package)
3. [  Conditional Relationships Without N+1 ](#conditional-relationships-without-n1)
4. [  Versioning Without Duplication ](#versioning-without-duplication)
5. [  Wrapping Metadata Consistently ](#wrapping-metadata-consistently)
6. [  Takeaways ](#takeaways)

  ![Laravel API Resources: Sparse Fieldsets, Conditional Relationships, and Stable Versioning](https://cdn.msaied.com/273/1a704012015f49cacee781eafd98d85c.png)

  #laravel   #api   #resources   #versioning  

 Laravel API Resources: Sparse Fieldsets, Conditional Relationships, and Stable Versioning 
===========================================================================================

     23 Jun 2026      3 min read    ![Mohamed Said](https://cdn.msaied.com/01KT78WE565VEMM3PSNQAAB0MJ.jpg)  Mohamed Said  

       Table of contents

1. [  01   Beyond toArray: Getting Serious About Laravel API Resources  ](#beyond-codetoarraycode-getting-serious-about-laravel-api-resources)
2. [  02   Sparse Fieldsets Without a Package  ](#sparse-fieldsets-without-a-package)
3. [  03   Conditional Relationships Without N+1  ](#conditional-relationships-without-n1)
4. [  04   Versioning Without Duplication  ](#versioning-without-duplication)
5. [  05   Wrapping Metadata Consistently  ](#wrapping-metadata-consistently)
6. [  06   Takeaways  ](#takeaways)

 Beyond `toArray`: Getting Serious About Laravel API Resources
-------------------------------------------------------------

Most Laravel codebases start with a `UserResource` that returns every column and calls it done. That works until a mobile client complains about payload size, a second API version ships, or a relationship silently triggers 200 extra queries. This article fixes all three.

---

Sparse Fieldsets Without a Package
----------------------------------

JSON:API specifies `?fields[users]=id,email` as a way for clients to request only the columns they need. You can implement a lightweight version natively.

```php
// app/Http/Resources/Concerns/SparseFieldset.php
trait SparseFieldset
{
    protected function sparseFields(array $all): array
    {
        $type = $this->resourceType();
        $requested = request()->query('fields', []);

        if (empty($requested[$type])) {
            return $all;
        }

        $allowed = array_flip(explode(',', $requested[$type]));
        return array_intersect_key($all, $allowed);
    }

    abstract protected function resourceType(): string;
}

```

```php
// app/Http/Resources/UserResource.php
class UserResource extends JsonResource
{
    use SparseFieldset;

    protected function resourceType(): string { return 'users'; }

    public function toArray(Request $request): array
    {
        return $this->sparseFields([
            'id'         => $this->id,
            'email'      => $this->email,
            'name'       => $this->name,
            'created_at' => $this->created_at,
        ]);
    }
}

```

Clients now send `GET /users?fields[users]=id,email` and receive a trimmed payload. No package required, no reflection magic.

---

Conditional Relationships Without N+1
-------------------------------------

`$this->whenLoaded()` is the correct primitive, but it only prevents serialisation of an unloaded relation — it does **not** load it. The loading decision must happen in the controller.

```php
// app/Http/Controllers/UserController.php
public function index(Request $request): AnonymousResourceCollection
{
    $includes = array_intersect(
        explode(',', $request->query('include', '')),
        ['posts', 'roles'] // allowlist
    );

    $users = User::query()
        ->when(in_array('posts', $includes), fn ($q) => $q->with('posts'))
        ->when(in_array('roles', $includes), fn ($q) => $q->with('roles'))
        ->paginate();

    return UserResource::collection($users);
}

```

```php
// Inside UserResource::toArray
'posts' => PostResource::collection($this->whenLoaded('posts')),
'roles' => RoleResource::collection($this->whenLoaded('roles')),

```

The controller owns the eager-loading decision; the resource owns the shape. The two concerns never bleed into each other.

---

Versioning Without Duplication
------------------------------

A common mistake is copying `UserResource` into `V2/UserResource` and diverging forever. Instead, extend and override only what changed.

```php
// app/Http/Resources/V2/UserResource.php
namespace App\Http\Resources\V2;

use App\Http\Resources\UserResource as V1UserResource;

class UserResource extends V1UserResource
{
    public function toArray(Request $request): array
    {
        return array_merge(parent::toArray($request), [
            'display_name' => $this->profile?->display_name,
            // 'email' removed in v2 for privacy
            'email' => $this->when(false, $this->email),
        ]);
    }
}

```

Route groups bind the correct resource class:

```php
Route::prefix('v1')->group(function () {
    Route::apiResource('users', V1\UserController::class);
});

Route::prefix('v2')->group(function () {
    Route::apiResource('users', V2\UserController::class);
});

```

Each versioned controller returns its own resource class. The V1 resource stays frozen; V2 inherits and overrides. When V3 arrives, it extends V2 the same way.

---

Wrapping Metadata Consistently
------------------------------

For non-paginated endpoints, `additional()` keeps envelope logic out of controllers:

```php
return UserResource::collection($users)
    ->additional([
        'meta' => ['version' => 'v2', 'generated_at' => now()->toIso8601String()],
    ]);

```

For paginated responses, `ResourceCollection` lets you override `paginationInformation` to rename or remove keys your clients don't expect.

---

Takeaways
---------

- Implement sparse fieldsets with a simple trait and an allowlist — no package needed.
- Keep eager-loading decisions in the controller; use `whenLoaded` in resources purely for conditional serialisation.
- Version resources by extension, not by copy-paste — freeze V1, extend into V2.
- Use `additional()` for consistent envelope metadata without polluting controller return statements.
- Always maintain an allowlist for `include` parameters to prevent arbitrary relationship traversal.

 Found this useful?

          [  ](https://twitter.com/intent/tweet?url=https%3A%2F%2Fmsaied.com%2Farticles%2Flaravel-api-resources-sparse-fieldsets-conditional-relationships-and-stable-versioning&text=Laravel+API+Resources%3A+Sparse+Fieldsets%2C+Conditional+Relationships%2C+and+Stable+Versioning) [  ](https://www.linkedin.com/sharing/share-offsite/?url=https%3A%2F%2Fmsaied.com%2Farticles%2Flaravel-api-resources-sparse-fieldsets-conditional-relationships-and-stable-versioning) 

 Frequently Asked Questions 
----------------------------

  3 questions  

     Q01  Does `whenLoaded()` prevent the N+1 query problem on its own?        `whenLoaded()` only skips serialising a relation that was not loaded — it does not trigger or suppress a query. You must eager-load the relation in the controller. If you skip eager loading, Eloquent will still lazy-load the relation when the resource accesses it, producing N+1 queries. 

      Q02  Is extending a V1 resource for V2 safe when V1 must stay stable?        Yes, as long as V1 controllers continue to return the V1 resource class. The V2 resource extends V1 but is only instantiated by V2 controllers. Changes to V2 never affect V1 responses because PHP's inheritance is one-directional. 

      Q03  Should sparse fieldset filtering happen in the resource or in the query?        For small payloads, filtering in the resource (after the query) is fine and keeps the database layer clean. For very wide tables or high-traffic endpoints, push the field list into a `select()` clause on the query builder to reduce data transfer from the database. Both approaches can coexist: select a safe subset in the query, then apply sparse fieldset filtering in the resource. 

  Continue reading

 More Articles 
---------------

 [ View all    ](https://msaied.com/articles) 

 [ ![Typed Enums as First-Class Domain Citizens in Laravel with PHP 8.3](https://cdn.msaied.com/282/71a8fc3e4cf4239b1bf6d38d57e0b985.png) laravel php8.3 enums 

### Typed Enums as First-Class Domain Citizens in Laravel with PHP 8.3

Go beyond simple enum labels. Learn how to attach behaviour, implement interfaces, and use backed enums as Elo...

  ![Mohamed Said](https://cdn.msaied.com/01KT78WE565VEMM3PSNQAAB0MJ.jpg)  Mohamed Said 

 24 Jun 2026     3 min read  

  Read    

 ](https://msaied.com/articles/typed-enums-as-first-class-domain-citizens-in-laravel-with-php-83) [ ![RAG in Laravel: pgvector, Embeddings, and Retrieval-Augmented Generation in Practice](https://cdn.msaied.com/281/8d2ac57c0e69d3ff9f1e68faf0e4d10c.png) laravel ai pgvector 

### RAG in Laravel: pgvector, Embeddings, and Retrieval-Augmented Generation in Practice

Build a production-ready RAG pipeline in Laravel using pgvector, OpenAI embeddings, and a clean retrieval laye...

  ![Mohamed Said](https://cdn.msaied.com/01KT78WE565VEMM3PSNQAAB0MJ.jpg)  Mohamed Said 

 24 Jun 2026     4 min read  

  Read    

 ](https://msaied.com/articles/rag-in-laravel-pgvector-embeddings-and-retrieval-augmented-generation-in-practice) [ ![Ship AI with Laravel: Failover, Queues, and Middleware for AI Agents](https://cdn.msaied.com/283/f0a6d6a6f22d9131bacb96bae1bfc10b.png) Laravel AI Agents Queues 

### Ship AI with Laravel: Failover, Queues, and Middleware for AI Agents

Learn how to make Laravel AI agents production-ready with automatic provider failover, background queue proces...

  ![Mohamed Said](https://cdn.msaied.com/01KT78WE565VEMM3PSNQAAB0MJ.jpg)  Mohamed Said 

 24 Jun 2026     3 min read  

  Read    

 ](https://msaied.com/articles/ship-ai-with-laravel-failover-queues-and-middleware-for-ai-agents) 

   [  ![Mohamed Said](https://cdn.msaied.com/01KT78WE565VEMM3PSNQAAB0MH.png)   Mohamed Said Laravel Backend Engineer  ](https://msaied.com)Senior Backend Engineer specializing in Laravel, scalable SaaS platforms, APIs, and cloud infrastructure. I build secure, high-performance web applications that help businesses grow.

Explore

- [Home](https://msaied.com)
- [Projects](https://msaied.com/projects)
- [Articles](https://msaied.com/articles)
- [Certificates](https://msaied.com/certificates)
- [Contact](https://msaied.com#contact-section)

Connect

- [   hello@msaied.com ](mailto:hello@msaied.com)
- [   +20 109 461 9204 ](tel:+201094619204)

© 2026 Mohamed Said. All rights reserved.

 [  ](https://github.com/EG-Mohamed) [  ](https://www.linkedin.com/in/msaiedm/) [  ](https://wa.me/201094619204) [  ](mailto:hello@msaied.com) [  ](https://drive.google.com/file/u/0/d/1MF20IPRJyzfy32mhEutjL5EpSls0w2Q8/view)
